package org.javers.shadow; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import org.javers.common.validation.Validate; import org.javers.core.commit.CommitMetadata; import org.javers.core.json.JsonConverter; import org.javers.core.metamodel.object.CdoSnapshot; import org.javers.core.metamodel.object.CdoSnapshotState; import org.javers.core.metamodel.object.GlobalId; import org.javers.core.metamodel.object.InstanceId; import org.javers.core.metamodel.type.*; import java.time.format.DateTimeParseException; import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; import static org.javers.core.metamodel.object.CdoSnapshotStateBuilder.cdoSnapshotState; /** * Stateful builder * * @author bartosz.walacik */ class ShadowGraphBuilder { private final JsonConverter jsonConverter; private final BiFunction<CommitMetadata, GlobalId, CdoSnapshot> referenceResolver; private boolean built = false; private Map<GlobalId, ShadowBuilder> builtNodes = new HashMap<>(); private final TypeMapper typeMapper; private final CommitMetadata rootContext; ShadowGraphBuilder(JsonConverter jsonConverter, BiFunction<CommitMetadata, GlobalId, CdoSnapshot> referenceResolver, TypeMapper typeMapper, CommitMetadata rootContext) { this.jsonConverter = jsonConverter; this.referenceResolver = referenceResolver; this.typeMapper = typeMapper; this.rootContext = rootContext; } Object buildDeepShadow(CdoSnapshot cdoSnapshot) { Validate.argumentIsNotNull(cdoSnapshot); switchToBuilt(); ShadowBuilder root = assembleShadowStub(cdoSnapshot); doWiring(); return root.getShadow(); } private void doWiring() { builtNodes.values().forEach(ShadowBuilder::wire); } private void switchToBuilt() { if (built) { throw new IllegalStateException("already built"); } built = true; } private ShadowBuilder assembleShallowReferenceShadow(InstanceId instanceId, EntityType shallowReferenceType) { CdoSnapshotState state = cdoSnapshotState().withPropertyValue(shallowReferenceType.getIdProperty(), instanceId.getCdoId()).build(); Object shadowStub = jsonConverter.fromJson(toJson(state), shallowReferenceType.getBaseJavaClass()); ShadowBuilder shadowBuilder = new ShadowBuilder(null, shadowStub); builtNodes.put(instanceId, shadowBuilder); return shadowBuilder; } private ShadowBuilder assembleShadowStub(CdoSnapshot cdoSnapshot) { ShadowBuilder shadowBuilder = new ShadowBuilder(cdoSnapshot, null); builtNodes.put(cdoSnapshot.getGlobalId(), shadowBuilder); JsonObject jsonElement = toJson(cdoSnapshot.getState()); mapCustomPropertyNamesToJavaOrigin(cdoSnapshot.getManagedType(), jsonElement); followReferences(shadowBuilder, jsonElement); shadowBuilder.withStub( deserializeObjectFromJsonElement(cdoSnapshot.getManagedType(), jsonElement)); return shadowBuilder; } private Object deserializeObjectFromJsonElement(ManagedType managedType, JsonObject jsonElement) { try { return jsonConverter.fromJson(jsonElement, managedType.getBaseJavaClass()); } catch(JsonSyntaxException | DateTimeParseException e) { return sanitizedDeserialization(jsonElement, managedType); } } private Object sanitizedDeserialization(JsonObject jsonElement, ManagedType managedType) { managedType.getProperties().forEach(p -> { try { jsonConverter.fromJson(jsonElement.get(p.getName()), p.getRawType()); } catch (Exception e) { jsonElement.remove(p.getName()); } }); return jsonConverter.fromJson(jsonElement, managedType.getBaseJavaClass()); } private void mapCustomPropertyNamesToJavaOrigin(ManagedType managedType, JsonObject jsonElement) { managedType.forEachProperty(javersProperty -> { if (javersProperty.hasCustomName()) { JsonElement value = jsonElement.get(javersProperty.getName()); jsonElement.remove(javersProperty.getName()); jsonElement.add(javersProperty.getOriginalName(), value); } }); } private void followReferences(ShadowBuilder currentNode, JsonObject jsonElement) { CdoSnapshot cdoSnapshot = currentNode.getCdoSnapshot(); cdoSnapshot.getManagedType().forEachProperty( property -> { if (cdoSnapshot.isNull(property)) { return; } if (property.getType() instanceof ManagedType) { GlobalId refId = (GlobalId) cdoSnapshot.getPropertyValue(property); ShadowBuilder target = createOrReuseNodeFromRef(refId, property); if (target != null) { currentNode.addReferenceWiring(property, target); } jsonElement.remove(property.getName()); } if (typeMapper.isContainerOfManagedTypes(property.getType()) || typeMapper.isKeyValueTypeWithManagedTypes(property.getType())) { EnumerableType propertyType = property.getType(); Object containerWithRefs = cdoSnapshot.getPropertyValue(property); if (!propertyType.isEmpty(containerWithRefs)) { currentNode.addEnumerableWiring(property, propertyType .map(containerWithRefs, (value) -> passValueOrCreateNodeFromRef(value, property), true)); jsonElement.remove(property.getName()); } } }); } private Object passValueOrCreateNodeFromRef(Object value, JaversProperty property) { if (value instanceof GlobalId) { return createOrReuseNodeFromRef((GlobalId)value, property); } return value; } private ShadowBuilder createOrReuseNodeFromRef(GlobalId globalId, JaversProperty property) { if (builtNodes.containsKey(globalId)) { return builtNodes.get(globalId); } if (property.isShallowReference()) { EntityType shallowReferenceType = property.getType() instanceof EntityType ? property.getType() : (EntityType)typeMapper.getJaversManagedType(globalId); if (shallowReferenceType.getIdProperty().getType() instanceof ValueObjectType) { //TODO don't know how to reconstruct Id in ShallowReference, which happened to be a ValueObject, return null; } else { return assembleShallowReferenceShadow((InstanceId) globalId, shallowReferenceType); } } CdoSnapshot cdoSnapshot = referenceResolver.apply(rootContext, globalId); if (cdoSnapshot != null) { return assembleShadowStub(cdoSnapshot); } return null; } private JsonObject toJson(CdoSnapshotState state) { return (JsonObject)jsonConverter.toJsonElement(state); } }