com.nordea.oss.copybook.serializers.CopyBookParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of copybook4java Show documentation
Show all versions of copybook4java Show documentation
CopyBook serializer and deserializer for Java where CopyBook lines are used to annotate a normal Java class
The newest version!
/*
* Copyright (c) 2015. Troels Liebe Bentsen
* Copyright (c) 2016. Nordea Bank AB
* Licensed under the MIT license (LICENSE.txt)
*/
package com.nordea.oss.copybook.serializers;
import com.nordea.oss.copybook.annotations.*;
import com.nordea.oss.copybook.converters.TypeConverter;
import com.nordea.oss.copybook.converters.TypeConverterConfig;
import com.nordea.oss.copybook.exceptions.CopyBookException;
import com.nordea.oss.copybook.exceptions.TypeConverterException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
// TODO: Add packed decimal - http://www.simotime.com/datapk01.htm
// TODO: Add default VALUE after PIC
public class CopyBookParser {
private static Pattern re_CopyBookLine = Pattern.compile("^\\s*(\\d+)\\s+([^\\s]+)((?:\\s+OCCURS\\s+\\d+(?:\\s+TO\\s+\\d+)?(?:\\s+TIMES)?)|(?:\\s+REDEFINES\\s+[^\\s]+))?(\\s+PIC\\s+[^\\s]+)?(\\s+DEPENDING\\s+ON\\s+[^\\s]+(?:\\s+IN\\s+[^\\s+]+)?)?\\s*\\.\\s*$");
private static Pattern re_Occurs = Pattern.compile("^\\s*OCCURS\\s+(?:(\\d+)\\s+TO\\s+)?(\\d+)(:?\\s+TIMES)?\\s*$");
private static Pattern re_Redefines = Pattern.compile("\\s*REDEFINES\\s+([^\\s]+)\\s*");
private static Pattern re_Pic = Pattern.compile("^\\s*PIC\\s+(S)?(X+|9+)(?:\\((\\d+)\\))?(?:V(9+)(?:\\((\\d+)\\))?)?\\s*$");
private static Pattern re_DependingOn = Pattern.compile("^\\s*DEPENDING\\s+ON\\s+([^\\s]+)(?:\\s+IN\\s+([^\\s]+))?\\s*$");
private Class extends CopyBookMapper> serializerClass = null;
private CopyBookSerializerConfig config = new CopyBookSerializerConfig();
public CopyBookParser(Class> type) {
this(type, false);
}
public CopyBookParser(Class> type, boolean debug) {
// Read copybook annotations and defaults
List copybookAnnotations = getAnnotationsRecursively(CopyBookDefaults.class, CopyBook.class);
copybookAnnotations.addAll(getAnnotationsRecursively(type, CopyBook.class));
for(CopyBook annotation : copybookAnnotations) {
if (!annotation.type().equals(CopyBookMapper.class)) {
this.serializerClass = annotation.type();
}
if (!annotation.charset().isEmpty()) {
config.setCharset(Charset.forName(annotation.charset()));
}
if(annotation.separatorChar() != 'G') { // Java sucks and does not support null as default value for annotations so we pick large G as this is å in EBCDIC and unlikely to be used as separator char
config.setSeparatorByte((byte)annotation.separatorChar());
}
if(annotation.bitmapBlockSize() != 0) {
config.setBitmapBlockSize(annotation.bitmapBlockSize());
}
if(annotation.strict()) {
config.setStrict(annotation.strict());
}
}
if(serializerClass == null) {
throw new CopyBookException("Serialization class missing");
}
config.setDebug(debug);
Map defaultTypeConverterMap = getTypeConvertersRecursively(CopyBookDefaults.class, config.getCharset(), config.isStrict());
config.setFields(walkClass(type, null, defaultTypeConverterMap, config.getCharset(), config.isStrict(), new HashMap<>()));
}
@SuppressWarnings("unchecked")
private List walkClass(Class> type, String copyBookName, Map inheritedTypeConverterMap, Charset charset, boolean strict, Map copyBookFieldNames) {
List results = new ArrayList<>();
CopyBook copyBookAnnotation = type.getAnnotation(CopyBook.class);
if(copyBookAnnotation == null) {
throw new CopyBookException("No copybook defined on this class");
}
// Overwrite type converts with what we find on the sub copybook
Map typeConverterMap = new HashMap<>(inheritedTypeConverterMap);
typeConverterMap.putAll(getTypeConvertersRecursively(type, charset, strict));
// Iterate over the class fields with CopyBookLine annotation
for (Field field : type.getDeclaredFields()) {
List copyBookLines = Arrays.stream(field.getAnnotationsByType(CopyBookLine.class)).map(CopyBookLine::value).collect(Collectors.toList());
copyBookLines.addAll(Arrays.stream(field.getAnnotationsByType(CopyBookLine1.class)).map(CopyBookLine1::value).collect(Collectors.toList()));
copyBookLines.addAll(Arrays.stream(field.getAnnotationsByType(CopyBookLine2.class)).map(CopyBookLine2::value).collect(Collectors.toList()));
copyBookLines.addAll(Arrays.stream(field.getAnnotationsByType(CopyBookLine3.class)).map(CopyBookLine3::value).collect(Collectors.toList()));
copyBookLines.addAll(Arrays.stream(field.getAnnotationsByType(CopyBookLine4.class)).map(CopyBookLine4::value).collect(Collectors.toList()));
copyBookLines.addAll(Arrays.stream(field.getAnnotationsByType(CopyBookLine5.class)).map(CopyBookLine5::value).collect(Collectors.toList()));
CopyBookRedefine copyBookRedefine = field.getDeclaredAnnotation(CopyBookRedefine.class);
String redefineOn = copyBookRedefine != null ? copyBookRedefine.on() : null;
String redefineMatch = copyBookRedefine != null ? copyBookRedefine.match() : null;
if(copyBookLines.size() > 0) {
String fieldName = type.getName() + "." + field.getName();
Class> fieldBaseType = field.getType().isArray() ? field.getType().getComponentType() : field.getType();
String fieldTypeName = getTypeClassSimpleName(fieldBaseType);
List names = new ArrayList<>();
if(copyBookName != null) {
names.add(copyBookName);
}
int size = 0;
int decimals = -1;
int minOccurs = -1;
int maxOccurs = -1;
String counterKey = null;
String copyBookType = null;
String redefines = null;
for(String copyBookLine : copyBookLines) {
Matcher copyBookLineMatcher = re_CopyBookLine.matcher(copyBookLine);
if (copyBookLineMatcher.find()) {
int level = Integer.parseInt(copyBookLineMatcher.group(1));
names.add(copyBookLineMatcher.group(2));
if (copyBookLineMatcher.group(3) != null) {
Matcher occursMatcher = re_Occurs.matcher(copyBookLineMatcher.group(3));
Matcher redefinesMatcher = re_Redefines.matcher(copyBookLineMatcher.group(3));
if (occursMatcher.find()) {
maxOccurs = Integer.parseInt(occursMatcher.group(2));
minOccurs = occursMatcher.group(1) != null ? Integer.parseInt(occursMatcher.group(1)) : maxOccurs;
} else if(redefinesMatcher.find()) {
redefines = redefinesMatcher.group(1);
} else {
throw new CopyBookException("Could not parse occurs section in copybook line for field '" + fieldName + "'");
}
}
if (copyBookLineMatcher.group(4) != null) {
Matcher picMatcher = re_Pic.matcher(copyBookLineMatcher.group(4));
if (picMatcher.find()) {
boolean signed = picMatcher.group(1) != null;
String mainType = picMatcher.group(2);
int mainSize = picMatcher.group(3) != null ? Integer.parseInt(picMatcher.group(3)) : mainType.length();
String decimalType = picMatcher.group(4) != null ? picMatcher.group(4) : "";
int decimalSize = picMatcher.group(5) != null ? Integer.parseInt(picMatcher.group(5)) : decimalType.length();
// Add decimals to size if they are set
size = mainSize + decimalSize;
decimals = decimalSize;
// Find type for this copybook line
if (mainType.startsWith("X")) { // String type
copyBookType = "String";
} else if (mainType.startsWith("9")) { // Signed number
if (!decimalType.isEmpty()) {
copyBookType = "Decimal";
} else {
copyBookType = "Integer";
}
if (signed) {
copyBookType = "Signed" + copyBookType;
}
} else {
throw new CopyBookException("Unknown PIC type for field '" + fieldName + "'");
}
} else {
throw new CopyBookException("Could not parse occurs section in copybook line for field '" + fieldName + "'");
}
}
if (copyBookLineMatcher.group(5) != null) {
Matcher dependingOnMatcher = re_DependingOn.matcher(copyBookLineMatcher.group(5));
if (dependingOnMatcher.find()) {
String dependedName = dependingOnMatcher.group(1);
String subFieldName = dependingOnMatcher.group(2);
List counterKeyNames = new ArrayList<>(names.subList(0, names.size() - 1));
if(subFieldName != null) {
if(names.size() > 1) {
// Go up one level and add subfield that the counter exists in
counterKeyNames.remove(counterKeyNames.size() - 1);
counterKeyNames.add(subFieldName);
} else {
throw new CopyBookException("IN only makes sense when you are in another level for field '" + fieldName + "'");
}
}
counterKeyNames.add(dependedName);
counterKey = counterKeyNames.stream().collect(Collectors.joining("."));
if(copyBookFieldNames.containsKey(counterKey)) {
copyBookFieldNames.get(counterKey).setCounter(true);
} else {
throw new CopyBookException("Could not find referenced counter " + counterKey + "for field '" + fieldName + "'");
}
} else {
throw new CopyBookException("Could not parse depending on section in copybook line for field '" + fieldName + "'");
}
}
} else {
throw new CopyBookException("Could not parse copybook line for field '" + fieldName + "'");
}
}
if((minOccurs > 0 || maxOccurs > 0) && !field.getType().isArray()) {
throw new CopyBookException("Trying to map array type to non array type for field '" + fieldName + "'");
}
// Find type converts that have been set on the field
Map fieldTypeConverterMap = new HashMap<>(typeConverterMap);
fieldTypeConverterMap.putAll(getTypeConvertersRecursively(fieldBaseType, charset, strict));
// Resolve typeConverter
TypeConverter typeConverter = null;
if(copyBookType != null) {
if (field.getType().isArray()) {
//TODO: Add ArrayTo example and test
typeConverter = fieldTypeConverterMap.get(copyBookType + "ArrayTo" + fieldTypeName + "Array");
}
if (typeConverter == null) {
typeConverter = fieldTypeConverterMap.get(copyBookType + "To" + fieldTypeName);
}
// Try to see if we have a convert for any of the interfaces this type implements
if (typeConverter == null) {
for (Class> aInterface : fieldBaseType.getInterfaces()) {
typeConverter = fieldTypeConverterMap.get(copyBookType + "To" + aInterface.getSimpleName());
if(typeConverter != null) {
try {
typeConverter = typeConverter.copy(type);
} catch (IllegalAccessException | InstantiationException e) {
throw new CopyBookException("Failed to copy type convert for this field '" + fieldName + "'", e);
}
}
}
}
} else {
if (field.getType().isArray()) {
//TODO: Add ArrayTo example and test
typeConverter = fieldTypeConverterMap.get("ArrayTo" + fieldTypeName + "Array");
}
if (typeConverter == null) {
typeConverter = fieldTypeConverterMap.get("To" + fieldTypeName);
}
}
if(typeConverter != null) {
try {
typeConverter.validate(fieldBaseType, size, decimals);
} catch (TypeConverterException ex) {
throw new CopyBookException(fieldName + ":", ex);
}
}
String name = names.stream().collect(Collectors.joining("."));
CopyBookField copyBookField = new CopyBookField(type, field, name, size, decimals, minOccurs, maxOccurs, copyBookLines, counterKey, typeConverter);
copyBookFieldNames.put(name, copyBookField);
copyBookField.setRedefines(getFullFieldName(names,redefines));
copyBookField.setRedefinedOn(getFullFieldName(names, redefineOn));
copyBookField.setRedefineMatch(redefineMatch);
// Did not find a type convert so lets see if it's another copybook class
if(typeConverter == null) {
if (fieldBaseType.getAnnotation(CopyBook.class) != null) {
copyBookField.setSubCopyBookFields(walkClass(fieldBaseType, name, typeConverterMap, charset, strict, copyBookFieldNames));
} else {
if(fieldBaseType.isPrimitive() || fieldBaseType.getName().startsWith("java")) {
if(copyBookType == null) {
throw new CopyBookException("Error mapping field '" + fieldName + "' as it is not defining a type in the copybook line");
} else {
throw new CopyBookException("Error mapping field '" + fieldName + "', could not find field converter defined for " + copyBookType + " to " + fieldTypeName);
}
} else {
if(copyBookType == null) {
throw new CopyBookException("CopyBook annotation not found on type for field '" + fieldName + "' or no field converter defined for " + fieldTypeName);
} else {
throw new CopyBookException("CopyBook annotation not found on type for field '" + fieldName + "' or no field converter defined for " + copyBookType + " to " + fieldTypeName);
}
}
}
}
results.add(copyBookField);
}
}
return results;
}
private String getTypeClassSimpleName(Class> type) {
switch (type.getName()) {
case "boolean": return "Boolean";
case "int": return "Integer";
case "long": return "Long";
case "double": return "Double";
case "bool": return "Bool";
case "char": return "Char";
case "byte": return "Byte";
case "void": return "Void";
default: return type.getSimpleName();
}
}
@SuppressWarnings("unchecked")
private List getAnnotationsRecursively(Class> type, Class annotationType) {
List results = new ArrayList<>();
for (Annotation annotation : type.getAnnotations()) {
if(annotationType.isInstance(annotation)) {
results.add((T)annotation);
} else if (annotation.annotationType().getAnnotation(annotationType) != null) {
results.addAll(getAnnotationsRecursively(annotation.annotationType(), annotationType));
}
}
return results;
}
private Map getTypeConvertersRecursively(Class> type, Charset charset, Boolean strict) {
Map results = new HashMap<>();
for (Annotation annotation : type.getAnnotations()) {
if(CopyBookFieldFormats.class.isInstance(annotation)) {
for(CopyBookFieldFormat fieldFormat : ((CopyBookFieldFormats)annotation).value()) {
results.put(fieldFormat.type().getSimpleName(), createTypeConverter(fieldFormat, charset, strict));
}
} else if(CopyBookFieldFormat.class.isInstance(annotation)) {
CopyBookFieldFormat fieldFormat = (CopyBookFieldFormat)annotation;
results.put(fieldFormat.type().getSimpleName(), createTypeConverter(fieldFormat, charset, strict));
} else if (annotation.annotationType().getAnnotation(CopyBookFieldFormat.class) != null) {
results.putAll(getTypeConvertersRecursively(annotation.annotationType(), charset, strict));
}
}
return results;
}
private TypeConverter createTypeConverter(CopyBookFieldFormat copyBookFieldFormat, Charset charset, Boolean strict) {
TypeConverterConfig config = new TypeConverterConfig();
config.setCharset(charset);
config.setRightPadding(copyBookFieldFormat.rightPadding());
config.setNullFillerChar(copyBookFieldFormat.nullFillerChar());
config.setPaddingChar(copyBookFieldFormat.paddingChar());
config.setSigningType(copyBookFieldFormat.signingType());
config.setDefaultValue((copyBookFieldFormat.defaultValue().isEmpty() || strict) ? null : copyBookFieldFormat.defaultValue());
config.setFormat(copyBookFieldFormat.format());
try {
TypeConverter typeConverter = copyBookFieldFormat.type().newInstance();
typeConverter.initialize(config);
return typeConverter;
} catch (InstantiationException | IllegalAccessException ex) {
throw new CopyBookException("Failed to load TypeConverterBase");
} catch (TypeConverterException e) {
throw new CopyBookException("Failed to initialize type convert", e);
}
}
private String getFullFieldName(List names, String name) {
List baseNames = new ArrayList<>(names.subList(0, names.size() - 1));
baseNames.add(name);
return baseNames.stream().collect(Collectors.joining("."));
}
public CopyBookSerializerConfig getConfig() {
return config;
}
public void setConfig(CopyBookSerializerConfig config) {
this.config = config;
}
public Class extends CopyBookMapper> getSerializerClass() {
return serializerClass;
}
public void setSerializerClass(Class extends CopyBookMapper> serializerClass) {
this.serializerClass = serializerClass;
}
}