Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
*/
public class FormatContext<T> {

private int offset;
private Class<T> dataType;
private Class<? extends FixedFormatter<T>> formatter;
private final int offset;
private final Class<T> dataType;
private final Class<? extends FixedFormatter<T>> formatter;

/**
* Creates a new format context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
* @since 1.0.0
*/
public class ByTypeFormatter implements FixedFormatter<Object> {
private FormatContext<?> context;
private final FormatContext<?> context;

private static final Map<Class<? extends Serializable>, Class<? extends FixedFormatter<?>>> KNOWN_FORMATTERS = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.ancientprogramming.fixedformat4j.format.impl;

import com.ancientprogramming.fixedformat4j.annotation.Field;
import com.ancientprogramming.fixedformat4j.annotation.Fields;
import com.ancientprogramming.fixedformat4j.annotation.Record;
import com.ancientprogramming.fixedformat4j.format.FixedFormatter;
import com.ancientprogramming.fixedformat4j.format.FormatContext;
import com.ancientprogramming.fixedformat4j.format.FormatInstructions;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.ancientprogramming.fixedformat4j.format.FixedFormatUtil.getFixedFormatterInstance;

/**
* JVM-level cache of per-class field metadata ({@link FieldDescriptor} lists).
*
* <p>The first call to {@link #get} for a class scans its annotations and builds one
* {@link FieldDescriptor} per effective {@code @Field}. Subsequent calls return the same
* immutable list without re-scanning.
*
* <p>Thread safety: {@code computeIfAbsent} guarantees that {@link #build} runs at most once per
* class key. Helper objects ({@link AnnotationScanner}, {@link FormatInstructionsBuilder},
* {@link RepeatingFieldSupport}) are created as local variables inside {@code build} so that
* concurrent builds of different classes never share mutable state.
*
* <p><strong>Note:</strong> this cache is never cleared. In multi-classloader environments
* (e.g. application servers with hot-reload, OSGi containers) old {@link Class} references may
* be retained here after their classloader is discarded, preventing garbage collection.
* In such environments consider using a {@link java.lang.ref.WeakReference}-based map instead.
*
* @author Jacob von Eyben - <a href="https://eybenconsult.com">https://eybenconsult.com</a>
* @since 1.7.1
*/
class ClassMetadataCache {

static final ClassMetadataCache INSTANCE = new ClassMetadataCache();

private final Map<Class<?>, List<FieldDescriptor>> cache = new ConcurrentHashMap<>();

List<FieldDescriptor> get(Class<?> clazz) {
return cache.computeIfAbsent(clazz, this::build);
}

private List<FieldDescriptor> build(Class<?> clazz) {
AnnotationScanner scanner = new AnnotationScanner();
FormatInstructionsBuilder instructionsBuilder = new FormatInstructionsBuilder();
RepeatingFieldSupport repeatingFieldSupport = new RepeatingFieldSupport();

List<FieldDescriptor> result = new ArrayList<>();
for (AnnotationTarget target : scanner.scan(clazz)) {
Field fieldAnnotation = target.annotationSource.getAnnotation(Field.class);
Fields fieldsAnnotation = target.annotationSource.getAnnotation(Fields.class);
if (fieldAnnotation != null) {
result.add(buildDescriptor(clazz, target, fieldAnnotation, true, scanner, instructionsBuilder, repeatingFieldSupport));
} else if (fieldsAnnotation != null) {
Field[] fields = fieldsAnnotation.value();
for (int i = 0; i < fields.length; i++) {
result.add(buildDescriptor(clazz, target, fields[i], i == 0, scanner, instructionsBuilder, repeatingFieldSupport));
}
}
}
return Collections.unmodifiableList(result);
}

@SuppressWarnings({"unchecked", "rawtypes"})
private FieldDescriptor buildDescriptor(
Class<?> clazz,
AnnotationTarget target,
Field fieldAnnotation,
boolean isLoadField,
AnnotationScanner scanner,
FormatInstructionsBuilder instructionsBuilder,
RepeatingFieldSupport repeatingFieldSupport) {

Class<?> datatype = instructionsBuilder.datatype(target.getter, fieldAnnotation);
repeatingFieldSupport.validateCount(target.getter, fieldAnnotation);
boolean isRepeating = fieldAnnotation.count() > 1;
boolean hasCustomFormatter = fieldAnnotation.formatter() != ByTypeFormatter.class;
boolean isNestedRecord = !isRepeating && !hasCustomFormatter && datatype.getAnnotation(Record.class) != null;

FormatContext<?> context = isRepeating ? null : instructionsBuilder.context(datatype, fieldAnnotation);
FormatInstructions formatInstructions = isRepeating ? null : instructionsBuilder.build(target.annotationSource, fieldAnnotation, datatype);
FixedFormatter<?> formatter = (isRepeating || isNestedRecord) ? null
: getFixedFormatterInstance(context.getFormatter(), context);

Method setter = resolveSetter(clazz, target.getter, datatype, scanner);

return new FieldDescriptor(target, setter, fieldAnnotation, datatype, context, formatInstructions,
formatter, isRepeating, isNestedRecord, isLoadField);
}

private Method resolveSetter(Class<?> clazz, Method getter, Class<?> datatype, AnnotationScanner scanner) {
String setterName = "set" + scanner.stripMethodPrefix(getter.getName());
try {
return clazz.getMethod(setterName, datatype);
} catch (NoSuchMethodException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.ancientprogramming.fixedformat4j.format.impl;

import com.ancientprogramming.fixedformat4j.annotation.Field;
import com.ancientprogramming.fixedformat4j.format.FixedFormatter;
import com.ancientprogramming.fixedformat4j.format.FormatContext;
import com.ancientprogramming.fixedformat4j.format.FormatInstructions;

import java.lang.reflect.Method;

/**
* Immutable bundle of all per-field metadata computed once per class and cached for reuse across
* every {@code load()} and {@code export()} call.
*
* <p>For repeating fields ({@code count > 1}), {@link #context}, {@link #formatInstructions}, and
* {@link #formatter} are {@code null} — the runtime delegates to {@link RepeatingFieldSupport}.
* For fields whose type is itself a {@code @Record}, {@link #formatter} is {@code null} and the
* runtime recurses into {@code FixedFormatManagerImpl}.
*
* @author Jacob von Eyben - <a href="https://eybenconsult.com">https://eybenconsult.com</a>
* @since 1.7.1
*/
class FieldDescriptor {

final AnnotationTarget target;
final Method setter;
final Field fieldAnnotation;
final Class<?> datatype;
final FormatContext<?> context;
final FormatInstructions formatInstructions;
final FixedFormatter<?> formatter;
final boolean isRepeating;
final boolean isNestedRecord;
/**
* {@code true} when this descriptor should participate in {@code load()} (i.e. its parsed value
* is written to the POJO via the setter). For plain {@code @Field} annotations this is always
* {@code true}. For {@code @Fields}, only the first annotation in the array is a load field;
* the remainder are export-only.
*/
final boolean isLoadField;

FieldDescriptor(
AnnotationTarget target,
Method setter,
Field fieldAnnotation,
Class<?> datatype,
FormatContext<?> context,
FormatInstructions formatInstructions,
FixedFormatter<?> formatter,
boolean isRepeating,
boolean isNestedRecord,
boolean isLoadField) {
this.target = target;
this.setter = setter;
this.fieldAnnotation = fieldAnnotation;
this.datatype = datatype;
this.context = context;
this.formatInstructions = formatInstructions;
this.formatter = formatter;
this.isRepeating = isRepeating;
this.isNestedRecord = isNestedRecord;
this.isLoadField = isLoadField;
}
}
Loading
Loading