Skip to content

Method: isImmutable(Object)

1: /*
2: * JOPA
3: * Copyright (C) 2024 Czech Technical University in Prague
4: *
5: * This library is free software; you can redistribute it and/or
6: * modify it under the terms of the GNU Lesser General Public
7: * License as published by the Free Software Foundation; either
8: * version 3.0 of the License, or (at your option) any later version.
9: *
10: * This library is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13: * Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public
16: * License along with this library.
17: */
18: package cz.cvut.kbss.jopa.sessions;
19:
20: import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException;
21: import cz.cvut.kbss.jopa.model.LoadState;
22: import cz.cvut.kbss.jopa.model.MultilingualString;
23: import cz.cvut.kbss.jopa.model.descriptors.Descriptor;
24: import cz.cvut.kbss.jopa.model.metamodel.EntityType;
25: import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification;
26: import cz.cvut.kbss.jopa.model.metamodel.Identifier;
27: import cz.cvut.kbss.jopa.model.metamodel.Metamodel;
28: import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingProxyFactory;
29: import cz.cvut.kbss.jopa.sessions.change.ChangeRecord;
30: import cz.cvut.kbss.jopa.sessions.change.ObjectChangeSet;
31: import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor;
32: import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory;
33: import cz.cvut.kbss.jopa.sessions.util.CloneConfiguration;
34: import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor;
35: import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils;
36: import cz.cvut.kbss.ontodriver.model.LangString;
37: import org.slf4j.Logger;
38: import org.slf4j.LoggerFactory;
39:
40: import java.lang.reflect.Field;
41: import java.math.BigDecimal;
42: import java.math.BigInteger;
43: import java.net.URI;
44: import java.net.URL;
45: import java.time.Duration;
46: import java.time.Instant;
47: import java.time.LocalDate;
48: import java.time.LocalDateTime;
49: import java.time.LocalTime;
50: import java.time.OffsetDateTime;
51: import java.time.OffsetTime;
52: import java.time.Period;
53: import java.time.ZoneOffset;
54: import java.time.ZonedDateTime;
55: import java.util.Collection;
56: import java.util.Date;
57: import java.util.Map;
58: import java.util.Objects;
59: import java.util.Set;
60: import java.util.stream.Collectors;
61: import java.util.stream.Stream;
62:
63: /**
64: * Builds clones used in transactions for tracking changes.
65: */
66: public class CloneBuilder {
67:
68: private static final Logger LOG = LoggerFactory.getLogger(CloneBuilder.class);
69:
70: private static final Set<Class<?>> IMMUTABLE_TYPES = getImmutableTypes();
71:
72: // Contains entities that are already cloned, so that we don't clone them again
73: private final RepositoryMap visitedEntities;
74:
75: private final Builders builders;
76:
77: private final LazyLoadingProxyFactory lazyLoaderFactory;
78:
79: private final AbstractUnitOfWork uow;
80:
81: public CloneBuilder(AbstractUnitOfWork uow) {
82: this.uow = uow;
83: this.visitedEntities = new RepositoryMap();
84: this.builders = new Builders();
85: this.lazyLoaderFactory = new LazyLoadingProxyFactory(uow);
86: }
87:
88: /**
89: * Builds clone of the given object.
90: *
91: * @param original Object
92: * @param cloneConfiguration Configuration for the cloning process
93: * @return The clone
94: * @throws NullPointerException If {@code original} is {@code null}
95: */
96: public Object buildClone(Object original, CloneConfiguration cloneConfiguration) {
97: Objects.requireNonNull(original);
98: Objects.requireNonNull(cloneConfiguration);
99: if (LOG.isTraceEnabled()) {
100: // Normally this is a bad practice, but since stringify could be quite costly, we want to avoid it if possible
101: LOG.trace("Cloning object {}.", uow.stringify(original));
102: }
103: return buildCloneImpl(null, null, original, cloneConfiguration);
104: }
105:
106: /**
107: * Builds clone of the given object.
108: * <p>
109: * This method differs from {@link #buildClone(Object, CloneConfiguration)} in that it accepts another argument
110: * which represents the owner of the built clone. This is useful in situations when we are cloning attributes
111: * directly, e.g. when lazily loading a field value.
112: *
113: * @param cloneOwner The owner of the created clone
114: * @param clonedField The field whose value is being cloned
115: * @param original The original to clone
116: * @param descriptor Entity descriptor
117: * @return The clone
118: * @throws NullPointerException If {@code cloneOwner}, {@code original} or {@code contextUri} is {@code null}
119: */
120: public Object buildClone(Object cloneOwner, Field clonedField, Object original, Descriptor descriptor) {
121: if (cloneOwner == null || original == null || descriptor == null) {
122: throw new NullPointerException();
123: }
124: if (LOG.isTraceEnabled()) {
125: // Normally this is a bad practice, but since stringify could be quite costly, we want to avoid it if possible
126: LOG.trace("Cloning object {} with owner {}", uow.stringify(original), uow.stringify(cloneOwner));
127: }
128: return buildCloneImpl(cloneOwner, clonedField, original, CloneConfiguration.withDescriptor(descriptor));
129: }
130:
131: private Object buildCloneImpl(Object cloneOwner, Field clonedField, Object original,
132: CloneConfiguration cloneConfiguration) {
133: assert original != null;
134: if (isOriginalInUoW(original)) {
135: return uow.getCloneForOriginal(original);
136: }
137: final Class<?> cls = original.getClass();
138: final boolean managed = isTypeManaged(cls);
139: final Descriptor descriptor = cloneConfiguration.getDescriptor();
140: if (managed) {
141: final Object visitedClone = getVisitedEntity(descriptor, original);
142: if (visitedClone != null) {
143: return visitedClone;
144: }
145: }
146: final AbstractInstanceBuilder builder = getInstanceBuilder(original);
147: Object clone = builder.buildClone(cloneOwner, clonedField, original, cloneConfiguration);
148: if (managed) {
149: // Register visited object before populating attributes to prevent infinite cloning cycles
150: putVisitedEntity(descriptor, original, clone);
151: final LoadStateDescriptor<Object> loadState = cloneLoadStateDescriptor(original, clone);
152: uow.getLoadStateRegistry().put(clone, loadState);
153: }
154: if (!builder.populatesAttributes() && !isImmutable(cls)) {
155: populateAttributes(original, clone, cloneConfiguration);
156: }
157: return clone;
158: }
159:
160: private LoadStateDescriptor<Object> cloneLoadStateDescriptor(Object original, Object clone) {
161: if (!uow.getLoadStateRegistry().contains(original)) {
162: uow.getLoadStateRegistry().put(original, LoadStateDescriptorFactory.createAllUnknown(original, (EntityType<? super Object>) getMetamodel().entity(original.getClass())));
163: }
164: final LoadStateDescriptor<Object> origLoadState = uow.getLoadStateRegistry().get(original);
165: return LoadStateDescriptorFactory.createCopy(clone, origLoadState);
166: }
167:
168: /**
169: * Clone all the attributes of the original and set the clone values. This also means cloning any relationships and
170: * their targets.
171: */
172: private void populateAttributes(Object original, Object clone, CloneConfiguration configuration) {
173: final Class<?> originalClass = original.getClass();
174: final EntityType<?> et = getMetamodel().entity(originalClass);
175: final LoadStateDescriptor<Object> loadState = uow.getLoadStateRegistry().get(clone);
176: // Ensure the identifier is cloned before any other attributes
177: // This prevents problems where circular references between entities lead to clones being registered with null identifier
178: cloneIdentifier(original, clone, et);
179: for (FieldSpecification<?, ?> fs : et.getFieldSpecifications()) {
180: if (fs == et.getIdentifier()) {
181: continue; // Already cloned
182: }
183: final Field f = fs.getJavaField();
184: final Object origVal = EntityPropertiesUtils.getFieldValue(f, original);
185: Object clonedValue;
186: if (loadState.isLoaded(fs) == LoadState.NOT_LOADED) {
187: clonedValue = lazyLoaderFactory.createProxy(clone, (FieldSpecification<? super Object, ?>) fs);
188: } else if (origVal == null) {
189: continue;
190: } else {
191: final Class<?> origValueClass = origVal.getClass();
192: if (isImmutable(origValueClass)) {
193: // The field is an immutable type
194: clonedValue = origVal;
195: } else if (IndirectWrapperHelper.requiresIndirectWrapper(origVal)) {
196: final Descriptor fieldDescriptor = getFieldDescriptor(f, originalClass, configuration.getDescriptor());
197: // Collection or Map
198: clonedValue = getInstanceBuilder(origVal).buildClone(clone, f, origVal,
199: CloneConfiguration.withDescriptor(fieldDescriptor)
200: .forPersistenceContext(configuration.isForPersistenceContext())
201: .addPostRegisterHandlers(configuration.getPostRegister()));
202: } else {
203: // Otherwise, we have a relationship, and we need to clone its target as well
204: if (isOriginalInUoW(origVal)) {
205: // If the reference is already managed
206: clonedValue = uow.getCloneForOriginal(origVal);
207: } else {
208: if (isTypeManaged(origValueClass)) {
209: final Descriptor fieldDescriptor =
210: getFieldDescriptor(f, originalClass, configuration.getDescriptor());
211: clonedValue = getVisitedEntity(configuration.getDescriptor(), origVal);
212: if (clonedValue == null) {
213: clonedValue = uow.registerExistingObject(origVal, new CloneRegistrationDescriptor(fieldDescriptor).postCloneHandlers(configuration.getPostRegister()));
214: }
215: } else {
216: clonedValue = buildClone(origVal, configuration);
217: }
218: }
219: }
220: }
221: EntityPropertiesUtils.setFieldValue(f, clone, clonedValue);
222: }
223: }
224:
225: private static void cloneIdentifier(Object original, Object clone, EntityType<?> et) {
226: final Identifier<?, ?> identifier = et.getIdentifier();
227: final Object idValue = EntityPropertiesUtils.getFieldValue(identifier.getJavaField(), original);
228: EntityPropertiesUtils.setFieldValue(identifier.getJavaField(), clone, idValue);
229: }
230:
231: private Descriptor getFieldDescriptor(Field field, Class<?> entityClass, Descriptor entityDescriptor) {
232: final EntityType<?> et = getMetamodel().entity(entityClass);
233: final FieldSpecification<?, ?> fieldSpec = et.getFieldSpecification(field.getName());
234: return entityDescriptor.getAttributeDescriptor(fieldSpec);
235: }
236:
237: /**
238: * Check if the given class is an immutable type.
239: * <p>
240: * Objects of immutable types do not have to be cloned, because they cannot be modified.
241: * <p>
242: * Note that this method does not do any sophisticated verification, it just checks if the specified class
243: * corresponds to a small set of predefined conditions, e.g. primitive class, enum, String.
244: *
245: * @param cls the class to check
246: * @return Whether the class represents immutable objects
247: */
248: static boolean isImmutable(Class<?> cls) {
249: return cls.isPrimitive() || cls.isEnum() || IMMUTABLE_TYPES.contains(cls);
250: }
251:
252: /**
253: * Checks if the specified object is immutable.
254: * <p>
255: * {@code null} is considered immutable, otherwise, this method just calls {@link #isImmutable(Class)}.
256: *
257: * @param object The instance to check
258: * @return immutability status
259: */
260: static boolean isImmutable(Object object) {
261:• return object == null || isImmutable(object.getClass());
262: }
263:
264: /**
265: * Merges the changes on clone into the original object.
266: *
267: * @param changeSet Contains changes to merge
268: */
269: public void mergeChanges(ObjectChangeSet changeSet) {
270: final Object original = changeSet.getOriginal();
271: final LoadStateDescriptor<?> loadStateDescriptor = uow.getLoadStateRegistry().get(original);
272: try {
273: for (ChangeRecord change : changeSet.getChanges()) {
274: Field f = change.getAttribute().getJavaField();
275: if (isImmutable(f.getType())) {
276: EntityPropertiesUtils.setFieldValue(f, original, change.getNewValue());
277: continue;
278: }
279: Object origVal = EntityPropertiesUtils.getFieldValue(f, original);
280: Object newVal = change.getNewValue();
281: if (newVal == null) {
282: EntityPropertiesUtils.setFieldValue(f, original, null);
283: } else {
284: getInstanceBuilder(newVal).mergeChanges(f, original, origVal, newVal);
285: }
286: loadStateDescriptor.setLoaded((FieldSpecification<? super Object, ?>) change.getAttribute(), LoadState.LOADED);
287: }
288: } catch (SecurityException e) {
289: throw new OWLPersistenceException(e);
290: }
291: }
292:
293: private Object getVisitedEntity(Descriptor descriptor, Object original) {
294: assert descriptor != null;
295: assert original != null;
296: return visitedEntities.get(descriptor, original);
297: }
298:
299: private void putVisitedEntity(Descriptor descriptor, Object original, Object clone) {
300: assert descriptor != null;
301: visitedEntities.add(descriptor, original, clone);
302: }
303:
304: AbstractInstanceBuilder getInstanceBuilder(Object toClone) {
305: return builders.getBuilder(toClone);
306: }
307:
308: boolean isTypeManaged(Class<?> cls) {
309: return uow.isEntityType(cls);
310: }
311:
312: boolean isOriginalInUoW(Object original) {
313: return uow.containsOriginal(original);
314: }
315:
316: Object getOriginal(Object clone) {
317: return uow.getOriginal(clone);
318: }
319:
320: Metamodel getMetamodel() {
321: return uow.getMetamodel();
322: }
323:
324: /**
325: * Resets the clone builder.
326: * <p>
327: * Especially resets the visited objects cache to make sure all the clones are built from scratch and are not
328: * affected by the previously built ones.
329: */
330: public void reset() {
331: visitedEntities.clear();
332: }
333:
334: /**
335: * Removes the specified instance from the clone builder's visited entities cache.
336: *
337: * @param instance The instance to remove (original object).
338: * @param descriptor Instance descriptor
339: */
340: public void removeVisited(Object instance, Descriptor descriptor) {
341: visitedEntities.remove(descriptor, instance);
342: }
343:
344: private static Set<Class<?>> getImmutableTypes() {
345: return Stream.of(Boolean.class,
346: Character.class,
347: Byte.class,
348: Short.class,
349: Integer.class,
350: Long.class,
351: Float.class,
352: Double.class,
353: BigInteger.class,
354: BigDecimal.class,
355: Void.class,
356: String.class,
357: URI.class,
358: URL.class,
359: LocalDate.class,
360: LocalTime.class,
361: LocalDateTime.class,
362: ZonedDateTime.class,
363: OffsetDateTime.class,
364: OffsetTime.class,
365: ZoneOffset.class,
366: Instant.class,
367: Duration.class,
368: Period.class,
369: LangString.class).collect(Collectors.toSet());
370: }
371:
372: private final class Builders {
373: private final AbstractInstanceBuilder defaultBuilder;
374: private final ManagedInstanceBuilder managedInstanceBuilder;
375: private final AbstractInstanceBuilder dateBuilder;
376: private final AbstractInstanceBuilder multilingualStringBuilder;
377: // Lists and Sets
378: private AbstractInstanceBuilder collectionBuilder;
379: private AbstractInstanceBuilder mapBuilder;
380:
381: private Builders() {
382: this.defaultBuilder = new DefaultInstanceBuilder(CloneBuilder.this, uow);
383: this.managedInstanceBuilder = new ManagedInstanceBuilder(CloneBuilder.this, uow);
384: this.dateBuilder = new DateInstanceBuilder(CloneBuilder.this, uow);
385: this.multilingualStringBuilder = new MultilingualStringInstanceBuilder(CloneBuilder.this, uow);
386: }
387:
388: private AbstractInstanceBuilder getBuilder(Object toClone) {
389: if (toClone instanceof Date) {
390: return dateBuilder;
391: }
392: if (toClone instanceof MultilingualString) {
393: return multilingualStringBuilder;
394: }
395: if (toClone instanceof Map) {
396: if (mapBuilder == null) {
397: this.mapBuilder = new MapInstanceBuilder(CloneBuilder.this, uow);
398: }
399: return mapBuilder;
400: } else if (toClone instanceof Collection) {
401: if (collectionBuilder == null) {
402: this.collectionBuilder = new CollectionInstanceBuilder(CloneBuilder.this, uow);
403: }
404: return collectionBuilder;
405: } else if (isTypeManaged(toClone.getClass())) {
406: return managedInstanceBuilder;
407: } else {
408: return defaultBuilder;
409: }
410: }
411: }
412: }