Skip to content

Commit

Permalink
implemented simple read and write operations
Browse files Browse the repository at this point in the history
  • Loading branch information
cphne committed Dec 1, 2023
1 parent ff804f7 commit 8b3e71a
Show file tree
Hide file tree
Showing 14 changed files with 816 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ buildNumber.properties
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
/.idea/
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,75 @@
# flat-file-parser
Implementation of a parser for flat file databases.

Flat file parser is a small java project that allows developers to parse and write flat files in a simple an
concise manner.

Provides an Interface to parse flat files and map its contents into java objects.
To represent and process the flat file definition/structure the project follows declarative approach make use of
annotations. Also allows to create flat files using the same definition as when parsing existing files.

## Prerequisites
Before you begin, ensure you have met the following requirements:
* You can support Java 17
* Since this project is not published to the central repository, make sure you can add .jar files to your classpath

## 'Installing'

To install flat file parser, follow these steps:
Download the latest *.jar release from the release tab and add the downloaded .jar to your classpath

## Using flat-file-parser

To use flat-file-parser, follow these steps:
* Create a Class with fields specifying the record format
* Use the factory to create a parser instance
* call `parse` to parse a flat file and map its data into a list of objects

```java
import cphne.flatfileparser.Field;
import cphne.flatfileparser.FlatFileParser;
import cphne.flatfileparser.ParserFactory;

import java.nio.file.Path;

class Main() {

static class DataObject {
@Field(start = 0, end = 3)
private int age;

@Field(start = 3, end = 10)
private String name;

// getters and setters...
}

public static void main(String[] args) {
FlatFileParser parser = ParserFactory.newInstance();
List<DataObject> data = parser.parse(Path.of("path/to/your/flatfile"), DataObject.class);
// process read data...
}
}
```

## Contributing to flat-file-parser
This is my first 'public' project on GitHub, I would be grateful for any constructive feedback. Should you have
suggestions about features, see the contact section of this readme.

For a general guideline, to contribute to flat-file-parser, follow these steps:

1. Fork this repository.
2. Create a branch: `git checkout -b <branch_name>`.
3. Make your changes and commit them: `git commit -m '<commit_message>'`
4. Push to the original branch: `git push origin <project_name>/<location>`
5. Create the pull request.


## Contact

If you want to contact me you can reach me at [email protected]

## License
<!--- If you're not sure which open license to use see https://choosealicense.com/--->

This project uses the following license: [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html).
46 changes: 46 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.github.cphne</groupId>
<artifactId>flat-file-parser</artifactId>
<version>1.0.0</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.4.11</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
28 changes: 28 additions & 0 deletions src/main/java/cphne/flatfileparser/Field.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cphne.flatfileparser;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Represents the definition of a field for a record in a flat file
* <p>
* Define the start and end positions of a field. Is required by {@link FlatFileParser} to parse records of a flat file
* and the referenced fields.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Field {
/**
*
* @return the start position of a field
*/
int start();

/**
*
* @return the end position of a field
*/
int end();
}
77 changes: 77 additions & 0 deletions src/main/java/cphne/flatfileparser/FlatFileParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cphne.flatfileparser;

import java.io.*;
import java.nio.file.Path;
import java.util.List;

/**
* Declares operations for parsing and writing of files formatted as a flat file
* <p>
* Parse functionality supports multiple variants of input types. Parses the file and maps its content into Objects
* of a provided type. The flat file format is specified in the Class the content should be mapped into via the
* {@link Field} annotation.
* <p>
* Transferring/writing objects to a flat file works similar to the parse operations. The Class of objects that
* should be written is required to provide the flat file format via the {@link Field} annotation.
*/
public interface FlatFileParser {

/**
* Parse a file and map the contained data to Objects
*
* @param file the file which contents to parse
* @param clazz the type the data should be mapped to
* @return {@code List<T>} the list of objects the data was mapped to
* @param <T> the type of objects in which the data will be mapped
* @throws IOException if the file cant be read or opened
* @throws ParserException if the parser cant parse the file
*/
<T> List<T> parse(File file, Class<T> clazz) throws IOException, ParserException;

/**
* Parse a file and map the contained data to Objects
*
* @param path the path to a file which contents to parse
* @param clazz the type the data should be mapped to
* @return {@code List<T>} the list of objects the data was mapped to
* @param <T> the type of objects in which the data will be mapped
* @throws IOException if the file cant be read or opened
* @throws ParserException if the parser cant parse the file
*/
<T> List<T> parse(Path path, Class<T> clazz) throws IOException, ParserException;

/**
* Parse data of an InputStream and map the contained data to Objects
*
* @param inputStream the stream which contains the contents to parse
* @param clazz the type the data should be mapped to
* @return {@code List<T>} the list of objects the data was mapped to
* @param <T> the type of objects in which the data will be mapped
* @throws IOException if the stream cant be read
* @throws ParserException if the parser cant parse the file
*/
<T> List<T> parse(InputStream inputStream, Class<T> clazz) throws IOException, ParserException;

/**
* Parse a content of a BufferedReader and map the contained data to Objects
*
* @param reader the reader which contents to parse
* @param clazz the type the data should be mapped to
* @return {@code List<T>} the list of objects the data was mapped to
* @param <T> the type of objects in which the data will be mapped
* @throws IOException if the reader cant be read
* @throws ParserException if the parser cant parse the file
*/
<T> List<T> parse(BufferedReader reader, Class<T> clazz) throws IOException, ParserException;

/**
* Convert a list of Objects to flat file
*
* @param stream OutputStream to write the data to
* @param data List of objects to convert
* @param <T> the type of objects which will be converted
* @throws IOException if the data cant be written to the stream
* @throws ParserException if the parser cant convert the objects to the specified flat file format
*/
<T> void write(OutputStream stream, List<T> data) throws IOException, ParserException;
}
144 changes: 144 additions & 0 deletions src/main/java/cphne/flatfileparser/FlatFileParserImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package cphne.flatfileparser;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Implementation of the {@link FlatFileParser} interface
*/
class FlatFileParserImpl implements FlatFileParser {

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FlatFileParserImpl.class);

/**
* The character to use to remaining space of a field
*/
private final String padCharacter;

/**
* Default constructor. Defines {@code " "} as the default {@link FlatFileParserImpl#padCharacter}
*/
public FlatFileParserImpl() {
this.padCharacter = " ";
}

/**
*
* @param padCharacter the Character to use for padding fields
*/
public FlatFileParserImpl(String padCharacter) {
this.padCharacter = padCharacter;
}

@Override
public <T> List<T> parse(File file, Class<T> clazz) throws IOException, ParserException {
return parse(file.toPath(), clazz);
}

@Override
public <T> List<T> parse(Path path, Class<T> clazz) throws IOException, ParserException {
try (BufferedReader bufferedReader = Files.newBufferedReader(path)) {
return parse(bufferedReader, clazz);
}
}

@Override
public <T> List<T> parse(InputStream inputStream, Class<T> clazz) throws IOException, ParserException {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
return parse(bufferedReader, clazz);
}
}

@Override
public <T> List<T> parse(BufferedReader reader, Class<T> clazz) throws IOException, ParserException {
List<T> targets = new ArrayList<>();
String line = reader.readLine();
while (line != null && !line.isBlank()) {
log.debug("Parsing line {}", line);
try {
targets.add(parse(line, clazz));
} catch (ReflectiveOperationException e) {
throw new ParserException(e);
}
line = reader.readLine();
}
return targets;
}

private <T> T parse(String line, Class<T> clazz) throws ReflectiveOperationException, ParserException {
LineParser<T> lineParser = new LineParserImpl<>(line, clazz.getDeclaredConstructor().newInstance());
List<java.lang.reflect.Field> fields = getFields(clazz);
if (fields.isEmpty()) {
throw new ParserException("Cant parse data, no field definitions defined for class %s.".formatted(clazz));
}
for (java.lang.reflect.Field field : fields) {
lineParser.parse(field);
}
return lineParser.getTarget();
}

private static <T> List<java.lang.reflect.Field> getFields(Class<T> clazz) {
return Arrays.stream(clazz.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Field.class))
.toList();
}


@Override
public <T> void write(OutputStream stream, List<T> dataList) throws IOException, ParserException {
if (dataList.isEmpty()) {
log.warn("Provided data to is empty, there is nothing to convert or write.");
}
for (T concreteObject : dataList) {
List<java.lang.reflect.Field> fields = getFields(concreteObject.getClass());
StringBuilder row = new StringBuilder();
for (java.lang.reflect.Field field : fields) {
try {
row.append(computeColumn(concreteObject, field));
} catch (ReflectiveOperationException e) {
throw new ParserException(e);
}
}
row.append("%n".formatted());
stream.write(row.toString().getBytes(StandardCharsets.UTF_8));
}
}

private <T> String computeColumn(
T concreteObject,
java.lang.reflect.Field field
) throws IllegalAccessException, InvocationTargetException {
Method getter = findGetter(field, concreteObject);
String data = getter.invoke(concreteObject).toString();
return data + padding(field, data.length());
}

private String padding(java.lang.reflect.Field field, int dataLength) {
int fieldLength = field.getAnnotation(Field.class).end() - field.getAnnotation(Field.class).start();
int unusedSpaceLength = fieldLength - dataLength;
return padCharacter.repeat(unusedSpaceLength);
}

private <T> Method findGetter(java.lang.reflect.Field field, T instance) {
Method setter = Arrays.stream(instance.getClass().getMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> m.getName().startsWith("get"))
.filter(m -> m.getName()
.equals("get%s%s".formatted(field.getName().substring(0, 1).toUpperCase(),
field.getName().substring(1)
)))
.findFirst()
.orElseThrow();
log.debug("Found getter for field {} with name {}.", field.getName(), setter.getName());
return setter;
}

}
Loading

0 comments on commit 8b3e71a

Please sign in to comment.