package com.psddev.cms.db; import com.google.common.base.Preconditions; import com.psddev.dari.db.ObjectIndex; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Query; import com.psddev.dari.db.Recordable; import com.psddev.dari.db.State; import com.psddev.dari.db.Trigger; import com.psddev.dari.util.ObjectUtils; import java.util.Collection; import java.util.stream.Stream; /** * Interface for defining custom behavior when copying objects through {@link #onCopy}. */ public interface Copyable extends Recordable { /** * Hook for defining custom behavior during object copy. Each of the object's implementation * and its {@link com.psddev.dari.db.Modification Modifications'} implementations of {@code onCopy} * are invoked. The invocations can occur in any order, so {@code onCopy} definitions should * not be interdependent. * <p> * The code defined within {@code onCopy} is executed on the copied {@link State} before it * is returned from {@link #copy}. * * @param source the Object to copy */ void onCopy(Object source); /** * Copies the given {@code source} object into an instance of the given * {@code targetType}. * <p> * If a target {@link ObjectType} is specified, the copied object will be converted * to the specified type, otherwise it will be of the same type as the object identified * by {@code source}. * * @param targetClass the class to which the copy should be converted * @param source the source object to be copied * @return the copy {@link State} after application of {@link #onCopy} */ @SuppressWarnings("unchecked") static <T> T copy(Class<T> targetClass, Object source) { Preconditions.checkNotNull(targetClass, "targetClass"); Preconditions.checkNotNull(source, "source"); // Query source object including invisible references. Cache is prevented which insures both that invisibles // are properly resolved and no existing instance of the source object becomes linked to the copy. // This prevents mutations to the new copy from affecting the original source object if it is subsequently saved. source = Query.fromAll().where("id = ?", source).resolveInvisible().noCache().first(); State sourceState = State.getInstance(source); ObjectType targetType = sourceState.getDatabase().getEnvironment().getTypeByClass(targetClass); Preconditions.checkState(targetType != null, "Copy failed! Could not determine copy target type!"); Object destination = targetType.createObject(null); State destinationState = State.getInstance(destination); Content.ObjectModification destinationContent = destinationState.as(Content.ObjectModification.class); // State#getRawValues must be used or invisible objects will not be included. destinationState.putAll(sourceState.getRawValues()); destinationState.setId(null); destinationState.setStatus(null); if (!ObjectUtils.equals(sourceState.getType(), targetType)) { destinationState.setType(sourceState.getType()); // Unset all visibility indexes defined by the source ObjectType Stream.concat( destinationState.getIndexes().stream(), destinationState.getDatabase().getEnvironment().getIndexes().stream()) .filter(ObjectIndex::isVisibility) .map(ObjectIndex::getFields) .flatMap(Collection::stream) .forEach(destinationState::remove); // update dari.visibilities while source ObjectType is set destinationState.getVisibilityAwareTypeId(); } destinationState.setType(targetType); // Clear existing paths destinationState.as(Directory.ObjectModification.class).clearPaths(); // Clear existing consumer Sites for (Site consumer : destinationState.as(Site.ObjectModification.class).getConsumers()) { destinationState.as(Directory.ObjectModification.class).clearSitePaths(consumer); } // Unset all visibility indexes defined by the target ObjectType Stream.concat( destinationState.getIndexes().stream(), destinationState.getDatabase().getEnvironment().getIndexes().stream()) .filter(ObjectIndex::isVisibility) .map(ObjectIndex::getFields) .flatMap(Collection::stream) .forEach(destinationState::remove); // update dari.visibilities while target ObjectType is set destinationState.getVisibilityAwareTypeId(); // Clear publishUser, updateUser, publishDate, and updateDate. destinationContent.setPublishUser(null); destinationContent.setUpdateUser(null); destinationContent.setUpdateDate(null); destinationContent.setPublishDate(null); // If it or any of its modifications are copyable, fire onCopy() destinationState.fireTrigger(new CopyTrigger(source)); return (T) destination; } /** * Copies the given {@code source} object. */ @SuppressWarnings("unchecked") static <T> T copy(T source) { return (T) copy(source.getClass(), source); } } /** * Executes {@link Copyable#onCopy} on the object and for each {@link com.psddev.dari.db.Modification}. */ @SuppressWarnings("unchecked") class CopyTrigger implements Trigger { private Object source; public CopyTrigger(Object source) { this.source = source; } @Override public void execute(Object object) { if (object instanceof Copyable) { ((Copyable) object).onCopy(source); } } }