diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a5cb482 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + target-branch: "develop" diff --git a/README.md b/README.md index fafbcb5..3194a4d 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,57 @@ Person problem pushing 'not a number' from row:'4' fieldIndex:'1' targetMethod:' Max age:43 ``` +## Single message parse +This example converts a single message into a single bean instance + +### StreamBeans code + +Mark a java bean with annotation `@CSVMarshaller` use lombok `@Data` to remove the boilerplate getter/setter +methods. The [RowMarshaller](runtime/src/main/java/com/fluxtion/extension/csvcompiler/RowMarshaller.java) is generated +at compile time by the csv-compiler annotation processor. + +```java + +@Data +@CsvMarshaller +public class Person { + private String name; + private int age; +} +``` + +The `RowMarshaller.parser(Person.class)` loads a SingleRowMarshaller for the Person javaBean. + + +A call to `SingleRowMarshaller.parse` will generate a single instance of Person if the message can be parsed. Validation +loggers can inject into the flow if called before the parser method. + + +```java + +public class SingleMessageParse { + + public static void main(String[] args) { + SingleRowMarshaller parser = RowMarshaller.parser(Person.class); + //headers - no output + parser.parse("name,age\n"); + + System.out.println("parsed:" + parser.parse("Jane,56\n")); + System.out.println("parsed:" + parser.parse("Isiah,12\n")); + System.out.println("parsed:" + parser.parse("Sky,42\n")); + } +} +``` + + +Application execution output: + +```text +parsed:SingleMessageParse.Person(name=Jane, age=56) +parsed:SingleMessageParse.Person(name=Isiah, age=12) +parsed:SingleMessageParse.Person(name=Sky, age=42) +``` + # Performance The CSV compiler annotation processor generates a marshaller during compilation. When deployed as a stateless function diff --git a/annotation-processor/pom.xml b/annotation-processor/pom.xml index 2020045..73f2d28 100644 --- a/annotation-processor/pom.xml +++ b/annotation-processor/pom.xml @@ -5,12 +5,12 @@ com.fluxtion.csv-compiler csv-compiler-parentpom - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT ../parent-pom/pom.xml csv-compiler-processor - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT csv-compiler :: annotation processor Build time annotation processor for generating CSV parsers diff --git a/annotation-processor/src/test/java/com/fluxtion/extension/csvcompiler/SingleParserTest.java b/annotation-processor/src/test/java/com/fluxtion/extension/csvcompiler/SingleParserTest.java new file mode 100644 index 0000000..c461c85 --- /dev/null +++ b/annotation-processor/src/test/java/com/fluxtion/extension/csvcompiler/SingleParserTest.java @@ -0,0 +1,32 @@ +package com.fluxtion.extension.csvcompiler; + +import com.fluxtion.extension.csvcompiler.annotations.CsvMarshaller; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SingleParserTest { + + + @Data + @AllArgsConstructor + @NoArgsConstructor + @CsvMarshaller + public static class Person { + private String name; + private int age; + } + + @Test + public void testSingleParse() { + SingleRowMarshaller parser = RowMarshaller.parser(Person.class); + //headers - no output + parser.parse("name,age\n"); + + Assertions.assertEquals(new Person("Jane", 56), parser.parse("Jane,56\n")); + Assertions.assertEquals(new Person("Isiah", 12), parser.parse("Isiah,12\n")); + Assertions.assertEquals(new Person("Sky", 42), parser.parse("Sky,42\n")); + } +} diff --git a/checker/pom.xml b/checker/pom.xml index 9cc1e3c..d6f749d 100644 --- a/checker/pom.xml +++ b/checker/pom.xml @@ -3,7 +3,7 @@ com.fluxtion.csv-compiler csv-compiler-parentpom - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT ../parent-pom/pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index e6b0f18..c3598cb 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -5,12 +5,12 @@ com.fluxtion.csv-compiler csv-compiler-parentpom - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT ../parent-pom/pom.xml csv-compiler-example - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT csv-compiler :: example diff --git a/example/src/main/java/com/fluxtion/extension/csvcompiler/example2/SingleMessageParse.java b/example/src/main/java/com/fluxtion/extension/csvcompiler/example2/SingleMessageParse.java new file mode 100644 index 0000000..94c5e44 --- /dev/null +++ b/example/src/main/java/com/fluxtion/extension/csvcompiler/example2/SingleMessageParse.java @@ -0,0 +1,26 @@ +package com.fluxtion.extension.csvcompiler.example2; + +import com.fluxtion.extension.csvcompiler.RowMarshaller; +import com.fluxtion.extension.csvcompiler.SingleRowMarshaller; +import com.fluxtion.extension.csvcompiler.annotations.CsvMarshaller; +import lombok.Data; + +public class SingleMessageParse { + + @Data + @CsvMarshaller + public static class Person { + private String name; + private int age; + } + + public static void main(String[] args) { + SingleRowMarshaller parser = RowMarshaller.parser(Person.class); + //headers - no output + parser.parse("name,age\n"); + + System.out.println("parsed:" + parser.parse("Jane,56\n")); + System.out.println("parsed:" + parser.parse("Isiah,12\n")); + System.out.println("parsed:" + parser.parse("Sky,42\n")); + } +} diff --git a/parent-pom/pom.xml b/parent-pom/pom.xml index ee0d368..034122f 100644 --- a/parent-pom/pom.xml +++ b/parent-pom/pom.xml @@ -4,7 +4,7 @@ com.fluxtion.csv-compiler csv-compiler-parentpom - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT pom csv-compiler :: parent pom diff --git a/pom.xml b/pom.xml index b642a05..9e61f0a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.fluxtion.extension csv-compiler-master - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT pom csv-compiler :: master diff --git a/runtime/pom.xml b/runtime/pom.xml index 439688b..1843260 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -5,12 +5,12 @@ com.fluxtion.csv-compiler csv-compiler-parentpom - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT ../parent-pom/pom.xml csv-compiler - 0.1.20-SNAPSHOT + 0.2.0-SNAPSHOT csv-compiler :: runtime Runtime library required to use generated CSV parsers diff --git a/runtime/src/main/java/com/fluxtion/extension/csvcompiler/OverwritingStringReader.java b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/OverwritingStringReader.java new file mode 100644 index 0000000..3cecbfb --- /dev/null +++ b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/OverwritingStringReader.java @@ -0,0 +1,235 @@ +package com.fluxtion.extension.csvcompiler; + +/* + * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import java.io.IOException; +import java.io.Reader; + +/** + * A character stream whose source is a string, the String can be replaced and the pointers reset within this class. + * + * This class is not thread safe, all reading and writing should be on the same thread. + * + * Derived from {@link java.io.StringReader} + * + */ + +class OverwritingStringReader extends Reader { + + private int length; + private String str; + private int next = 0; + private int mark = 0; + + /** + * Creates a new string reader. + * + * @param s String providing the character stream. + */ + public OverwritingStringReader(String s) { + this.length = s.length(); + this.str = s; + } + + public OverwritingStringReader(){ + this(""); + } + + public void append(String stringToAppend) { + this.length = stringToAppend.length(); + this.str = stringToAppend; + this.next = 0; + this.mark = 0; + } + + /** Check to make sure that the stream has not been closed */ + private void ensureOpen() throws IOException { + if (str == null) + throw new IOException("Stream closed"); + } + + /** + * Reads a single character. + * + * @return The character read, or -1 if the end of the stream has been + * reached + * + * @throws IOException If an I/O error occurs + */ + public int read() throws IOException { + synchronized (lock) { + ensureOpen(); + if (next >= length) + return -1; + return str.charAt(next++); + } + } + + /** + * Reads characters into a portion of an array. + * + *

If {@code len} is zero, then no characters are read and {@code 0} is + * returned; otherwise, there is an attempt to read at least one character. + * If no character is available because the stream is at its end, the value + * {@code -1} is returned; otherwise, at least one character is read and + * stored into {@code cbuf}. + * + * @param cbuf {@inheritDoc} + * @param off {@inheritDoc} + * @param len {@inheritDoc} + * + * @return {@inheritDoc} + * + * @throws IndexOutOfBoundsException {@inheritDoc} + * @throws IOException {@inheritDoc} + */ + public int read(char[] cbuf, int off, int len) throws IOException { + synchronized (lock) { + ensureOpen(); + checkFromIndexSize(off, len, cbuf.length); + if (len == 0) { + return 0; + } + if (next >= length) + return -1; + int n = Math.min(length - next, len); + str.getChars(next, next + n, cbuf, off); + next += n; + return n; + } + } + + public static void checkFromIndexSize(int fromIndex, int size, int length) { + if ((length | fromIndex | size) < 0 || size > length - fromIndex) + throw new IndexOutOfBoundsException( String.format("Range [%s, %The {@code n} parameter may be negative, even though the + * {@code skip} method of the {@link Reader} superclass throws + * an exception in this case. Negative values of {@code n} cause the + * stream to skip backwards. Negative return values indicate a skip + * backwards. It is not possible to skip backwards past the beginning of + * the string. + * + *

If the entire string has been read or skipped, then this method has + * no effect and always returns {@code 0}. + * + * @param n {@inheritDoc} + * + * @return {@inheritDoc} + * + * @throws IOException {@inheritDoc} + */ + public long skip(long n) throws IOException { + synchronized (lock) { + ensureOpen(); + if (next >= length) + return 0; + // Bound skip by beginning and end of the source + long r = Math.min(length - next, n); + r = Math.max(-next, r); + next += (int)r; + return r; + } + } + + /** + * Tells whether this stream is ready to be read. + * + * @return True if the next read() is guaranteed not to block for input + * + * @throws IOException If the stream is closed + */ + public boolean ready() throws IOException { + synchronized (lock) { + ensureOpen(); + return true; + } + } + + /** + * Tells whether this stream supports the mark() operation, which it does. + */ + public boolean markSupported() { + return false; + } + + /** + * Marks the present position in the stream. Subsequent calls to reset() + * will reposition the stream to this point. + * + * @param readAheadLimit Limit on the number of characters that may be + * read while still preserving the mark. Because + * the stream's input comes from a string, there + * is no actual limit, so this argument must not + * be negative, but is otherwise ignored. + * + * @throws IllegalArgumentException If {@code readAheadLimit < 0} + * @throws IOException If an I/O error occurs + */ + public void mark(int readAheadLimit) throws IOException { + if (readAheadLimit < 0){ + throw new IllegalArgumentException("Read-ahead limit < 0"); + } + synchronized (lock) { + ensureOpen(); + mark = next; + } + } + + /** + * Resets the stream to the most recent mark, or to the beginning of the + * string if it has never been marked. + * + * @throws IOException If an I/O error occurs + */ + public void reset() throws IOException { + synchronized (lock) { + ensureOpen(); + next = mark; + } + } + + /** + * Closes the stream and releases any system resources associated with + * it. Once the stream has been closed, further read(), + * ready(), mark(), or reset() invocations will throw an IOException. + * Closing a previously closed stream has no effect. This method will block + * while there is another thread blocking on the reader. + */ + public void close() { + synchronized (lock) { + str = null; + } + } +} + diff --git a/runtime/src/main/java/com/fluxtion/extension/csvcompiler/RowMarshaller.java b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/RowMarshaller.java index caa2efc..255eb57 100644 --- a/runtime/src/main/java/com/fluxtion/extension/csvcompiler/RowMarshaller.java +++ b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/RowMarshaller.java @@ -147,6 +147,14 @@ static RowMarshaller load(Class clazz) { throw new RuntimeException("unable to find RowMarshaller registered with ServiceLoader, class:" + clazz); } + /** + * Creates a SingleRowMarshaller for this {@link RowMarshaller} + * @return SingleRowMarshaller + */ + default SingleRowMarshaller parser(){ + return new SingleRowMarshaller<>(this); + } + /** * Transforms a masrshalled bean using an injected {@link Stream} operation. The input is read from reader path * and written to the writer path, The internal reader and writer are close when this operation terminates. @@ -193,6 +201,10 @@ default void transform(Reader reader, Writer writer, UnaryOperator> tr }); } + static SingleRowMarshaller parser(Class dataClass){ + return load(dataClass).parser(); + } + @SneakyThrows static void transform( Class dataClass, diff --git a/runtime/src/main/java/com/fluxtion/extension/csvcompiler/SingleRowMarshaller.java b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/SingleRowMarshaller.java new file mode 100644 index 0000000..1240ccd --- /dev/null +++ b/runtime/src/main/java/com/fluxtion/extension/csvcompiler/SingleRowMarshaller.java @@ -0,0 +1,39 @@ +package com.fluxtion.extension.csvcompiler; + +import java.util.Iterator; + +/** + * Wraps a {@link RowMarshaller} to parse single messages with a call to {@link #parse(String)} + * + * @param The target type of the {@link RowMarshaller} + */ +public class SingleRowMarshaller { + + private final OverwritingStringReader overwritingStringReader = new OverwritingStringReader(); + private final RowMarshaller rowMarshaller; + private final Iterator iterator; + + public SingleRowMarshaller(RowMarshaller rowMarshaller) { + this.rowMarshaller = rowMarshaller; + iterator = rowMarshaller.iterator(overwritingStringReader); + } + + /** + * Tries to marshall a message into an instance of the target type of the {@link RowMarshaller} + * @param message the input message to process + * + * @return A marshalled instance or null if the message could not be transformed into a valid instance of the target type + */ + public T parse(String message) { + overwritingStringReader.append(message); + return iterator.hasNext() ? iterator.next() : null; + } + + public void writeHeaders(StringBuilder builder) { + rowMarshaller.writeHeaders(builder); + } + + public void writeInputHeaders(StringBuilder builder) { + rowMarshaller.writeInputHeaders(builder); + } +}