1. Introduction

The RIS file format is one of the common formats used to exchange date between citation managers. It was initially developed by the commercial company Research Information Systems (RIS). It is used by numerous citation managers and digital libraries, some of which are e.g. Zotero or EndNote. For more details about the RIS file format, and it’s history, refer to the respective Wikipedia article.

KRis is a kotlin implementation that aims to be used as a library for Kotlin and Java projects. It is available on GitHub and can be used via gradle or maven dependency management. KRis started as a fork of JRis, which was developed by Gianluca Colaianni (fastluca).

KRis requires Java Platform 17 or later.

2. Usage

KRis can be used

  • to export instances of RisRecord to a text file conforming with the RIS format

  • to import RIS records from a text file and convert each of them to a RisRecord.

KRis offers an API that should be usable both from Kotlin and from Java. It offers a number of utility functions that allow you to easily handle your use case.

KRis offers methods to process RIS records in a non-blocking or blocking way.

3. Using KRis from Kotlin

Using KRis from kotlin allows us to profit from syntactic sugar provided by kotlin like data classes with default arguments for creating RisRecords or extension functions with receivers for calling the actual export/import methods.

3.1. Export: RIS records → text file

Let’s assume we have a number of bibliographic references we want to export them into a file in RIS format: We first have to convert each reference into a [RisRecord].

Instantiating bibliographic references as RisRecord [kotlin]
val record1 = RisRecord(
    type = RisType.JOUR,
    authors = mutableListOf("Shannon, Claude E."),
    publicationYear = "1948/07//",
    title = "A Mathematical Theory of Communication",
    secondaryTitle = "Bell System Technical Journal",
    startPage = "379",
    endPage = "423",
    volumeNumber = "27"
)
val record2 = RisRecord(
    type = RisType.JOUR,
    authors = mutableListOf("Turing, Alan Mathison"),
    publicationYear = "1948/07//",
    periodicalNameFullFormatJO = "Proc. of London Mathematical Society",
    title = "On computable numbers, with an application to the Entscheidungsproblem",
    secondaryTitle = "Bell System Technical Journal",
    startPage = "230",
    endPage = "265",
    volumeNumber = "47",
    issue = "1",
    primaryDate = "1937"
)

3.1.1. List: Blocking conversion

We can convert the records as a list into a list of strings. This happens in a blocking manner, and it is up to us what we want to do with the list of lines.

Converting a list of RisRecords in a blocking manner into a list of RIS lines [kotlin]
    val lines: List<String> = listOf(record1, record2).toRisLines()

We may want to influence how the RIS tags are sorted in the file. We can do that by providing a list of RIS tag names (as string) in the order we’d like to have them in the file.

Converting a list of RisRecords applying custom sort in a blocking manner into a list of RIS lines [kotlin]
    val sort: List<String> = listOf("SP", "EP", "T1")
    val lines: List<String> = listOf(record1, record2).toRisLines(sort)

All the subsequent method calls optionally accept a list of RIS tag names, specifying the tag sort order. We will not explicitly mention this option in the subsequent examples.

You can get a complete list of the RisTags available:

Getting a list of all RisTags (list of strings) [kotlin]
    // import ch.difty.kris.risTags
    val namesOfAllRisTags = risTagNames

If we want to write the list of lines into a file, we can do that manually, e.g.

Writing the list of lines into a file [kotlin]
    val lines: List<String> = listOf(record1, record2).toRisLines()

    // writing the lines to file, one of many possibilities
    val content: String = lines.joinToString(separator = "")
    File("export.ris").writeText(content)

To make this process easier, KRis offers some extension functions providing some syntactic sugar. We can use the accept extension function with a writer, a file, an output stream or a path as receiver respectively:

Writing the list of RisRecords in a blocking manner into a buffered writer [kotlin]
    val writer = File.createTempFile("export.ris", null, null).bufferedWriter()

    writer.accept(listOf(record1, record2))
Writing the list of RisRecords in a blocking manner into a file [kotlin]
    val file = File.createTempFile("export.ris", null, null)

    file.accept(listOf(record1, record2))
Writing the list of RisRecords in a blocking manner into an output stream [kotlin]
    val outputStream = File.createTempFile("export.ris", null, null).outputStream()

    outputStream.accept(listOf(record1, record2))
Writing the list of RisRecords in a blocking manner into a path [kotlin]
    "export.ris".accept(listOf(record1, record2))

3.1.2. Flow: Non-Blocking conversion

If you want to work with non-blocking reactive streams, you can work with flows:

Converting a flow of RisRecords in a non-blocking manner into a flow of Strings [kotlin]
    val flow = flowOf(record1, record2)

    val risLines: Flow<String> = flow.toRisLines()
Writing flow of RIS lines into file (example) [kotlin]
    val risLines: Flow<String> = flowOf(record1, record2).toRisLines()

    runBlocking {
        val content: String = risLines.toList().joinToString(separator = "")
        File("export.ris").writeText(content)
    }

3.2. Import: Text file → RIS records

Assume we have a text file import.ris with one or more RIS records similar to the following example:

import.ris
TY  - JOUR
AU  - Hjortebjerg,D.
AU  - Nybo Andersen,AM.
AU  - Ketzel,M.
AU  - Raaschou-Nielsen,O.
AU  - Sørensen,M.
PY  - 2018
TI  - Exposure to traffic noise and air pollution and risk for febrile seizure: a cohort study.
JO  - Scand J Work Environ Health. 2018 Mar 25. pii: 3724. doi.
ID  - 29574476
DO  - 10.5271/sjweh.3724
M1  - 9300
L2  - https://www.ncbi.nlm.nih.gov/pubmed/29574476
ER  -

3.2.1. Reading from File

If we have a reader, file, input stream or simply the path of the file, we can use the method process on the receiver. We will receive a list of [RisRecord] instances.

Reading RIS records from BufferedReader [kotlin]
    val bufferedReader: BufferedReader = File("import.ris").bufferedReader()

    val records: List<RisRecord> = bufferedReader.process()
Reading RIS records from File [kotlin]
    val file: File = File("import.ris")

    val records: List<RisRecord> = file.process()
Reading RIS records from InputStream [kotlin]
    val inputStream: FileInputStream = File("import.ris").inputStream()

    val records: List<RisRecord> = inputStream.process()
Reading RIS records from Path [kotlin]
    val records: List<RisRecord> = "import.ris".process()

3.2.2. RIS file lines in memory

Lists/Sequences: blocking conversion

In case we have the file content already in memory, we can pass it as a list or sequence.

Processing RIS records as list of strings [kotlin]
    val lines: List<String> = TODO()

    val records: List<RisRecord> = lines.toRisRecords()
Processing RIS records as sequence of strings [kotlin]
    val lineSequence: Sequence<String> = TODO()

    val records: Sequence<RisRecord> = lineSequence.toRisRecords(GlobalScope)
Flow: non-blocking conversion
Processing RIS records as flow of strings [kotlin]
    val lineFlow: Flow<String> = TODO()

    val recordFlow: Flow<RisRecord> = lineFlow.toRisRecords()

    runBlocking {
        val records: List<RisRecord> = recordFlow.toList()
    }

4. Using KRis from Java

Building RisRecords in Java is more cumbersome than in kotlin, as we cannot simply use the default arguments of the data class or comfortably use extension functions.

4.1. Export: RIS records → text file

We can however use the builder to only specify the fields we actually need.

Instantiating bibliographic references as RisRecord [Java]
    private final RisRecord record1 = new RisRecord.Builder()
        .type(RisType.JOUR)
        .authors(List.of("Shannon, Claude E."))
        .publicationYear("1948/07//")
        .title("A Mathematical Theory of Communication")
        .secondaryTitle("Bell System Technical Journal")
        .startPage("379")
        .endPage("423")
        .volumeNumber("27")
        .build();

    private final RisRecord record2 = new RisRecord.Builder()
        .type(RisType.JOUR)
        .authors(List.of("Turing, Alan Mathison"))
        .publicationYear("1948/07//")
        .periodicalNameFullFormatJO("Proc. of London Mathematical Society")
        .title("On computable numbers, with an application to the Entscheidungsproblem")
        .secondaryTitle("Bell System Technical Journal")
        .startPage("230")
        .endPage("265")
        .volumeNumber("47")
        .issue("1")
        .primaryDate("1937")
        .build();

4.1.1. List: Blocking conversion

We can convert the records as a list into a list of strings. This happens in a blocking manner, and it is up to us what we want to do with the list of lines.

Converting a list of RisRecords in a blocking manner into a list of RIS lines [Java]
        final List<RisRecord> records = List.of(record1, record2);

        List<String> lines = KRis.buildFromList(records);

We may want to influence how the RIS tags are sorted in the file. We can do that by providing a list of RIS tag names (as string) in the order we’d like to have them in the file.

Converting a list of RisRecords applying custom sort in a blocking manner into a list of RIS lines [Java]
        final List<RisRecord> records = List.of(record1, record2);
        final List<String> sort = List.of("SP", "EP", "T1");

        List<String> lines = KRis.buildFromList(records, sort);

All the subsequent method calls optionally accept a list of RIS tag names, specifying the tag sort order. We will not explicitly mention this option in the subsequent examples.

You can get a complete list of the RisTags available:

Getting a list of all RisTags (list of strings) [Java]
        List<String> namesOfAllRisTags = KRis.risTagNames();

4.1.2. Observable: Non-blocking conversion

Working with KRis in non-blocking manner in Java involves RxJava2.

Converting an observable of RisRecords in non-blocking manner into a file [Java]
    void fromObservable() throws IOException {
        final List<RisRecord> records = List.of(record1, record2);
        final Observable<RisRecord> observable = Observable.fromIterable(records);

        final BufferedWriter writer = new BufferedWriter(new FileWriter("export.ris"));

        KRis
            .exportObservable(observable)
            .doFinally(() -> closeWriter(writer))
            .blockingSubscribe(writer::append);
    }

    private void closeWriter(final BufferedWriter writer) {
        try {
            writer.close();
        } catch (Exception ex) {
        }
    }

To make the process of exporting to files easier, KRis offers some dedicated functions in the utility class KRisIO. We can use the export functions with a writer, a file, an output stream or a path as receiver respectively:

Writing the list of RisRecords in a blocking manner into a buffered writer [Java]
        final List<RisRecord> records = List.of(record1, record2);

        try (final BufferedWriter writer = new BufferedWriter(new FileWriter("export.ris"))) {
            KRisIO.export(records, writer);
        }
Writing the list of RisRecords in a blocking manner into a file [Java]
        final List<RisRecord> records = List.of(record1, record2);

        final File file = new File("export.ris");

        KRisIO.export(records, file);
Writing the list of RisRecords in a blocking manner into an output stream [Java]
        final List<RisRecord> records = List.of(record1, record2);

        try (OutputStream s = new BufferedOutputStream(new FileOutputStream("export.ris"))) {
            KRisIO.export(records, s);
        }
Writing the list of RisRecords in a blocking manner into a path [Java]
        final List<RisRecord> records = List.of(record1, record2);

        KRisIO.export(records, "export.ris");

4.2. Import: Text file → RIS records

Assume we have a text file import.ris with one or more RIS records similar to the following example:

import.ris
TY  - JOUR
AU  - Hjortebjerg,D.
AU  - Nybo Andersen,AM.
AU  - Ketzel,M.
AU  - Raaschou-Nielsen,O.
AU  - Sørensen,M.
PY  - 2018
TI  - Exposure to traffic noise and air pollution and risk for febrile seizure: a cohort study.
JO  - Scand J Work Environ Health. 2018 Mar 25. pii: 3724. doi.
ID  - 29574476
DO  - 10.5271/sjweh.3724
M1  - 9300
L2  - https://www.ncbi.nlm.nih.gov/pubmed/29574476
ER  -

4.2.1. Reading from File

If we have a reader, file, input stream or simply the path of the file, we can use the method process on KRisIO. We will receive a list of [RisRecord] instances.

Reading RIS records from BufferedReader [Java]
        try (final BufferedReader reader = new BufferedReader(new FileReader("import.ris"))) {
            final List<RisRecord> records = KRisIO.process(reader);
        }
Reading RIS records from File [Java]
        final File file = new File("import.ris");
        final List<RisRecord> records = KRisIO.process(file);
Reading RIS records from InputStream [Java]
        try (InputStream s = new BufferedInputStream(new FileInputStream("import.ris"))) {
            final List<RisRecord> records = KRisIO.process(s);
        }
Reading RIS records from Path [Java]
        final List<RisRecord> records = KRisIO.process("import.ris");

While this is sufficient to process reasonably small files, you may prefer consuming streams for processing large files.

Reading RIS records as stream from BufferedReader [Java]
        try (final BufferedReader reader = new BufferedReader(new FileReader("import.ris"))) {
            final Stream<RisRecord> records = KRisIO.processToStream(reader);
        }
Reading RIS records as stream from File [Java]
        final File file = new File("import.ris");
        final Stream<RisRecord> records = KRisIO.processToStream(file);
Reading RIS records as stream from InputStream [Java]
        try (InputStream s = new BufferedInputStream(new FileInputStream("import.ris"))) {
            final Stream<RisRecord> records = KRisIO.processToStream(s);
        }
Reading RIS records as stream from Path [Java]
        final Stream<RisRecord> records = KRisIO.processToStream("import.ris");

4.2.2. RIS file lines in memory

Lists: Blocking conversion

In case we have the file content already in memory, we can pass it as a list.

Processing RIS records as list of strings [Java]
        final List<String> lines = List.of(); // TODO
        List<RisRecord> records = KRis.processList(lines);
Observable : Non-blocking conversion
Converting an observable of ris line strings in non-blocking manner into RisRecords [Java]
        final List<RisRecord> risRecords = new ArrayList<>();
        final List<String> lines = new ArrayList<>();
        final Observable<String> observable = Observable.fromIterable(lines);

        KRis
            .processObservables(observable)
            .blockingSubscribe(risRecords::add);