All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.obsidiandynamics.blackstrom.codec.ContentMapper Maven / Gradle / Ivy

The newest version!
package com.obsidiandynamics.blackstrom.codec;

import static com.obsidiandynamics.func.Functions.*;

import java.util.*;

import com.obsidiandynamics.func.*;

/**
 *  Provides functionality for encapsulating arbitrary object content into {@link Variant}
 *  containers, and assisting in the mapping from a {@link Variant} back to the original 
 *  object content. 

* * A {@link ContentMapper} serves as a registry of {@link Unpacker} implementations * and version-to-class mappings. All operations on {@link Variant}s must be performed * in the context of a suitably configured {@link ContentMapper}.

* * When a {@link Variant} is captured, a content type and version pair * (captured in a {@link ContentHandle}) is resolved for the given content object * by consulting the mappings stored herein. These are then written out as part of * the object's wire representation during the subsequent serialization process.

* * Upon deserialization, the wire form is parsed to a limited extent — noting * the inline content type and version headers, but keeping the content-specific payload * elements in an intermediate {@link PackedForm} (which varies depending on the codec), * without attempting to map the serialized content back to its native Java class. * The {@link PackedForm} is encapsulated inside the returned {@link MonoVariant}. In * the case of a {@link PolyVariant}, multiple {@link PackedForm}s may be * encapsulated.

* * When a {@link Variant} is mapped to a Java class following deserialization * (by invoking {@link Variant#map(ContentMapper)}), the {@link ContentMapper} is first * consulted to resolve a corresponding mapping for the stored content type and * version pair, which yields the concrete class type (if a * configured mapping exists). This class type together with the {@link PackedForm} is * then handed to an appropriate {@link Unpacker}, which completes the deserialization * process and maps the packed content to its terminal Java class form, reconstructing * the original object graph. If no mapping is configured for the packed content type * and version, it is assumed that the content is unsupported, and a {@code null} is * silently returned.

* * A {@link Variant} may be of the type {@link PolyVariant}, which houses multiple * {@link MonoVariant}s. This allows the mapping process to enumerate over the variants * in a fallback manner, trying the first variant then advancing to the next, until either * all variants are exhausted (yielding a {@code null}) or a supported content type and * version is located (yielding the reconstituted object).

* * The specifics of the (de)serialization will depend on the particular codec employed. * The appropriate (de)serializers must be registered with the codec library prior to * (de)serializing {@link Variant} containers. Furthermore, the mapping process following * a deserialization requires a suitable {@link Unpacker} implementation. The latter must * be registered directly with the {@link ContentMapper} for every supported codec, as * unpacking may require capabilities beyond those natively supported by codec library. * For example, deserialization may acquire and release pooled resources, making two-pass * deserialization impossible without the special handling provided by an * {@link Unpacker}.

* * Once configured with unpackers and version mappings, this class is thread-safe. * * @see Variant * @see MonoVariant * @see PolyVariant * @see PackedForm * @see Unpacker */ public final class ContentMapper { private final Map, Unpacker> unpackers = new HashMap<>(); private final Map typeToVersions = new LinkedHashMap<>(); private final Map, VersionMapping> classToVersion = new HashMap<>(); /** * Thrown when registering version mappings in non-increasing order. I.e. it is required * that the successor version mapping must be registered after its predecessor, not before it. */ public static final class IllegalMappingException extends IllegalArgumentException { private static final long serialVersionUID = 1L; IllegalMappingException(String m) { super(m); } } /** * Thrown when a precise version mapping could not be resolved for a captured content * object. There must be a one-to-one relationship between captured classes and * version mappings. */ public static final class NoSuchMappingException extends IllegalArgumentException { private static final long serialVersionUID = 1L; NoSuchMappingException(String m) { super(m); } } /** * Thrown if an insufficient number of variants were supplied to a strict captor. In other words, * not all supported versions were accounted for by the invoking application. W */ public static final class InsufficientVariantsException extends IllegalArgumentException { private static final long serialVersionUID = 1L; InsufficientVariantsException(String m) { super(m); } } /** * Thrown if the variants supplied to a strict captor featured mixed content types. * If the use of heterogenous content types is required (for example, when the content * type must be altered as part of a version upgrade), use a relaxed captor * instead. */ public static final class MixedContentTypesException extends IllegalArgumentException { private static final long serialVersionUID = 1L; MixedContentTypesException(String m) { super(m); } } /** * Thrown by a strict captor when the supplied variants are in non-decreasing order * of version number. It is a requirement of a strict captor that the latest version of the * content object is provided first, then the next fallback version, and so on. */ public static final class NonDecreasingContentVersionsException extends IllegalArgumentException { private static final long serialVersionUID = 1L; NonDecreasingContentVersionsException(String m) { super(m); } } /** * Holds all {@link VersionMapping}s for a given content type. */ private static final class VersionMappings { private final List list = new ArrayList<>(); void add(VersionMapping mapping) { if (list.isEmpty()) { list.add(mapping); } else { final var preceding = list.get(list.size() - 1); final var newVersion = mapping.handle.getVersion(); final var precedingVersion = preceding.handle.getVersion(); mustBeGreater(newVersion, precedingVersion, withMessage(() -> "Next mapping (v" + newVersion + ") is not ahead of the preceding (v" + precedingVersion + ")", IllegalMappingException::new)); list.add(mapping); } } @Override public String toString() { return list.toString(); } void ensureSufficientMappings(int supplied) { mustBeGreaterOrEqual(supplied, list.size(), withMessage(() -> "Insufficient variants supplied; expected: " + list.size() + ", got: " + supplied, InsufficientVariantsException::new)); } } /** * Maps a content type and version to a concrete class type. */ static final class VersionMapping { final VersionMappings mappings; final ContentHandle handle; final Class contentClass; VersionMapping(VersionMappings mappings, ContentHandle handle, Class contentClass) { this.mappings = mappings; this.handle = handle; this.contentClass = contentClass; } @Override public String toString() { return handle.getVersion() + " -> " + contentClass.getName(); } } public ContentMapper() { registerDefaultVersions(); } private void registerDefaultVersions() { withVersion(Nil.getContentHandle(), Nil.class); } /** * Provides a string dump of all registered version mappings. * * @return A dump of registered mappings as a {@link String}. */ public String printVersions() { return typeToVersions.toString(); } public ContentMapper withUnpacker(Unpacker unpacker) { mustExist(unpacker, "Unpacker cannot be null"); final var packedClass = unpacker.getPackedType(); mustBeFalse(unpackers.containsKey(packedClass), withMessage(() -> "Duplicate unpacker for class " + packedClass.getName(), IllegalArgumentException::new)); unpackers.put(Classes.cast(packedClass), unpacker); return this; } public ContentMapper withVersion(ContentHandle contentHandle, Class contentClass) { mustExist(contentHandle, "Content handle cannot be null"); return withVersion(contentHandle.getType(), contentHandle.getVersion(), contentClass); } public ContentMapper withVersion(String contentType, int contentVersion, Class contentClass) { ContentHandle.validateContentType(contentType); ContentHandle.validateContentVersion(contentVersion); mustExist(contentClass, "Content class cannot be null"); final var existingMapping = classToVersion.get(contentClass); mustBeNull(existingMapping, withMessage(() -> "A mapping already exists for content " + contentClass, IllegalMappingException::new)); final var mappings = getOrCreateVersionMappings(contentType); final var mapping = new VersionMapping(mappings, new ContentHandle(contentType, contentVersion), contentClass); mappings.add(mapping); classToVersion.put(contentClass, mapping); return this; } private VersionMapping checkedGetMapping(Class cls) { return mustExist(classToVersion, cls, "No mapping for %s", NoSuchMappingException::new); } public interface Captor { Variant capture(Object content); Variant capture(Object... contentItems); } public MonoVariant capture(Object content) { mustExist(content, "Content cannot be null"); final var mapping = checkedGetMapping(content.getClass()); final var handle = mapping.handle; return new MonoVariant(handle, null, content); } public final class StandardRelaxedCaptor implements Captor { @Override public PolyVariant capture(Object content) { final var v = ContentMapper.this.capture(content); return new PolyVariant(v); } @Override public PolyVariant capture(Object... contentItems) { mustExist(contentItems, "Content items cannot be null"); mustBeGreater(contentItems.length, 0, illegalArgument("Content items cannot be empty")); final var variants = new MonoVariant[contentItems.length]; for (var i = 0; i < contentItems.length; i++) { variants[i] = ContentMapper.this.capture(contentItems[i]); } return new PolyVariant(variants); } } private final StandardRelaxedCaptor relaxedCaptor = new StandardRelaxedCaptor(); public StandardRelaxedCaptor relaxed() { return relaxedCaptor; } public final class CompactRelaxedCaptor implements Captor { @Override public MonoVariant capture(Object content) { return ContentMapper.this.capture(content); } @Override public Variant capture(Object... contentItems) { mustExist(contentItems, "Content items cannot be null"); mustBeGreater(contentItems.length, 0, illegalArgument("Content items cannot be empty")); if (contentItems.length == 1) { return capture(contentItems[0]); } else { return relaxedCaptor.capture(contentItems); } } } private final CompactRelaxedCaptor compactRelaxedCaptor = new CompactRelaxedCaptor(); public CompactRelaxedCaptor compactRelaxed() { return compactRelaxedCaptor; } public final class StandardStrictCaptor implements Captor { @Override public PolyVariant capture(Object content) { mustExist(content, "Content cannot be null"); final var mapping = checkedGetMapping(content.getClass()); mapping.mappings.ensureSufficientMappings(1); return new PolyVariant(new MonoVariant(mapping.handle, null, content)); } @Override public PolyVariant capture(Object... contentItems) { mustExist(contentItems, "Content items cannot be null"); mustBeGreater(contentItems.length, 0, illegalArgument("Content items cannot be empty")); final var variants = new MonoVariant[contentItems.length]; final var content0 = contentItems[0]; final var mapping0 = checkedGetMapping(content0.getClass()); mapping0.mappings.ensureSufficientMappings(contentItems.length); variants[0] = new MonoVariant(mapping0.handle, null, content0); var lastVersion = mapping0.handle.getVersion(); for (var i = 1; i < contentItems.length; i++) { final var content = contentItems[i]; final var mapping = checkedGetMapping(content.getClass()); final var _i = i; final var _lastVersion = lastVersion; lastVersion = mustBeLess(mapping.handle.getVersion(), lastVersion, withMessage(() -> "Content items should be arranged in decreasing order of version; v" + mapping.handle.getVersion() + " at index " + _i + " is later than v" + _lastVersion + " at index " + (_i - 1), NonDecreasingContentVersionsException::new)); mustBeEqual(mapping0.handle.getType(), mapping.handle.getType(), withMessage(() -> "Mixed content types unsupported; expected: " + mapping0.handle.getType() + " at index " + _i + ", got: " + mapping.handle.getType(), MixedContentTypesException::new)); variants[i] = new MonoVariant(mapping.handle, null, content); } return new PolyVariant(variants); } } private final StandardStrictCaptor strictCaptor = new StandardStrictCaptor(); public StandardStrictCaptor strict() { return strictCaptor; } public final class CompactStrictCaptor implements Captor { @Override public MonoVariant capture(Object content) { mustExist(content, "Content cannot be null"); final var mapping = checkedGetMapping(content.getClass()); mapping.mappings.ensureSufficientMappings(1); return new MonoVariant(mapping.handle, null, content); } @Override public Variant capture(Object... contentItems) { mustExist(contentItems, "Content items cannot be null"); mustBeGreater(contentItems.length, 0, illegalArgument("Content items cannot be empty")); if (contentItems.length == 1) { return capture(contentItems[0]); } else { return strictCaptor.capture(contentItems); } } } private final CompactStrictCaptor compactStrictCaptor = new CompactStrictCaptor(); public CompactStrictCaptor compactStrict() { return compactStrictCaptor; } public Object map(Variant variant) { return mustExist(variant, "Variant cannot be null").map(this); } Unpacker checkedGetUnpacker(Class packedFormClass) { return mustExist(unpackers, packedFormClass, "No unpacker for %s", IllegalStateException::new); } VersionMapping mappingForHandle(ContentHandle handle) { final var mappings = typeToVersions.get(handle.getType()); if (mappings != null) { final var desiredVersion = handle.getVersion(); final var numMappings = mappings.list.size(); for (var mappingIndex = numMappings; --mappingIndex >= 0; ) { final var mapping = mappings.list.get(mappingIndex); final var mappedVersion = mapping.handle.getVersion(); if (mappedVersion == desiredVersion) { return mapping; } else if (mappedVersion < desiredVersion) { return null; } } } return null; } private VersionMappings getOrCreateVersionMappings(String contentType) { return typeToVersions.computeIfAbsent(contentType, __ -> new VersionMappings()); } @Override public String toString() { return ContentMapper.class.getSimpleName() + " [typeToVersions=" + typeToVersions + ", unpackers=" + unpackers + "]"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy