/* * Copyright 2004-2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.compass.core.converter.mapping.osem; import java.lang.reflect.Array; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.compass.core.CompassException; import org.compass.core.Property; import org.compass.core.Resource; import org.compass.core.accessor.Getter; import org.compass.core.accessor.Setter; import org.compass.core.config.CompassConfigurable; import org.compass.core.config.CompassSettings; import org.compass.core.converter.ConversionException; import org.compass.core.converter.mapping.CollectionResourceWrapper; import org.compass.core.converter.mapping.ResourceMappingConverter; import org.compass.core.converter.mapping.ResourcePropertyConverter; import org.compass.core.engine.utils.ResourceHelper; import org.compass.core.mapping.Mapping; import org.compass.core.mapping.ResourceMapping; import org.compass.core.mapping.ResourcePropertyMapping; import org.compass.core.mapping.osem.ClassMapping; import org.compass.core.mapping.osem.ClassIdPropertyMapping; import org.compass.core.mapping.osem.ObjectMapping; import org.compass.core.mapping.osem.OsemMapping; import org.compass.core.marshall.MarshallingContext; import org.compass.core.marshall.MarshallingEnvironment; import org.compass.core.spi.InternalResource; import org.compass.core.spi.ResourceKey; import org.compass.core.util.ClassUtils; import org.compass.core.util.proxy.extractor.ProxyExtractorHelper; import org.compass.core.util.reflection.ReflectionConstructor; import org.compass.core.util.reflection.ReflectionFactory; /** * @author kimchy */ public class ClassMappingConverter implements ResourceMappingConverter, CompassConfigurable { /** * Under this key within the context the root class mapping can be found. */ public static final String ROOT_CLASS_MAPPING_KEY = "$rcmk"; /** * Disable internal mappings is a marker within the context if internal mappings should not * be marshalled. * * <p>Internal mappings are disabled for inner components (not root classes) when support * unmarshall is set to <code>false</code>. */ public static final String DISABLE_INTERNAL_MAPPINGS = "$dim"; private static final Object DISABLE_INTERNAL_MAPPINGS_MARK = new Object(); public static final String DISABLE_UID_MARSHALLING = "$disableUID"; private Map<String, ReflectionConstructor> cachedConstructors = new ConcurrentHashMap<String, ReflectionConstructor>(); private ProxyExtractorHelper proxyExtractorHelper; public void configure(CompassSettings settings) throws CompassException { proxyExtractorHelper = new ProxyExtractorHelper(); proxyExtractorHelper.configure(settings); } public boolean marshall(Resource resource, Object root, Mapping mapping, MarshallingContext context) throws ConversionException { if (root instanceof Resource) { Resource rootResource = (Resource) root; resource.copy(rootResource); ((InternalResource) resource).addUID(); return true; } // first store some important original context Object disableInternalMappings = context.getAttribute(DISABLE_INTERNAL_MAPPINGS); // perform the unmarshalling boolean store = doMarshall(resource, root, mapping, context); // restore the context context.setAttribute(DISABLE_INTERNAL_MAPPINGS, disableInternalMappings); return store; } protected boolean doMarshall(Resource resource, Object root, Mapping mapping, MarshallingContext context) throws ConversionException { ClassMapping classMapping = (ClassMapping) mapping; root = proxyExtractorHelper.initializeProxy(root); Object disableInternalMapings = context.getAttribute(DISABLE_INTERNAL_MAPPINGS); // Note that even if a component is root, it will not be root when // treated as a component (the binding part of the configuration takes // care and "unroots" it) if (classMapping.isRoot()) { doSetBoost(resource, root, classMapping, context); context.setAttribute(ROOT_CLASS_MAPPING_KEY, classMapping); context.setAttribute(DISABLE_INTERNAL_MAPPINGS, null); } else { if (!classMapping.isSupportUnmarshall()) { context.setAttribute(DISABLE_INTERNAL_MAPPINGS, DISABLE_INTERNAL_MAPPINGS_MARK); } } // handle null values if (root == null) { if (!classMapping.isSupportUnmarshall()) { return false; } if (!context.handleNulls()) { return false; } if (classMapping.getIdMappings().length == 0) { throw new ConversionException("Component mapping [" + classMapping.getAlias() + "] used within a collection/array and has null value, in such cases please define at least one id mapping on it"); } // go over all the ids and put a null value in it (just so we keep the order) boolean store = false; for (Mapping id : classMapping.getResourceIdMappings()) { store |= id.getConverter().marshall(resource, context.getResourceFactory().getNullValue(), id, context); } return store; } if (classMapping.isPoly() && classMapping.getPolyClass() == null) { // store the poly class only for root mappings when we don't support unmarshalling // and for all classes when we do support unmarshalling if (classMapping.isSupportUnmarshall() || classMapping.isRoot()) { storePolyClass(resource, root, classMapping, context); } } if (classMapping.isSupportUnmarshall() && root.getClass().isEnum()) { storeEnumName(resource, root, classMapping, context); } // check if we already marshalled this objecy under this alias // if we did, there is no need to completly marhsall it again // When we support *do* unmarshall, it is important that theses will have ids, since based // on them we will unmarshall correctly // When we *do not* support unmarshall, we don't care about ids. Therefore, we can also mark // marshalled based on object identity and support cyclic support for components without ids. if (classMapping.getIdMappings().length > 0) { IdsAliasesObjectKey idObjKey = new IdsAliasesObjectKey(classMapping, root); if (!idObjKey.hasNullId) { Object marshalled = context.getMarshalled(idObjKey); if (marshalled != null) { // we already marshalled this object, if we don't support unmarhsall, just return // otherwise only marshall its ids and return if (!classMapping.isSupportUnmarshall()) { return true; } if (classMapping.isFilterDuplicates()) { Mapping[] ids = classMapping.getIdMappings(); boolean store = false; for (int i = 0; i < ids.length; i++) { store |= ids[i].getConverter().marshall(resource, idObjKey.idsValues[i], ids[i], context); } return store; } } else { // we did not marshall this object, cache it for later checks context.setMarshalled(idObjKey, root); if (!classMapping.isSupportUnmarshall()) { context.setMarshalled(new IdentityAliasedObjectKey(classMapping.getAlias(), root), root); } } } } else if (!classMapping.isSupportUnmarshall()) { IdentityAliasedObjectKey key = new IdentityAliasedObjectKey(classMapping.getAlias(), root); Object marshalled = context.getMarshalled(key); if (marshalled != null) { return true; } context.setMarshalled(key, root); } // perform full marshalling of the object into the resource boolean store = false; for (Iterator mappingsIt = classMapping.mappingsIt(); mappingsIt.hasNext();) { context.setAttribute(MarshallingEnvironment.ATTRIBUTE_CURRENT, root); OsemMapping m = (OsemMapping) mappingsIt.next(); Object value; if (m.hasAccessors()) { Getter getter = ((ObjectMapping) m).getGetter(); value = getter.get(root); } else { value = root; } store |= m.getConverter().marshall(resource, value, m, context); } // marshall the uid last if (classMapping.isRoot() && !context.hasAttribute(DISABLE_UID_MARSHALLING)) { ((InternalResource) resource).addUID(); } // resotore the disable internal mappings flag context.setAttribute(DISABLE_INTERNAL_MAPPINGS, disableInternalMapings); return store; } public Object unmarshall(Resource resource, Mapping mapping, MarshallingContext context) throws ConversionException { ClassMapping classMapping = (ClassMapping) mapping; ResourceKey resourceKey = null; // handle a cache of all the unmarshalled objects already, used for // cyclic references when using reference mappings or component mappings // with ids if (classMapping.isRoot()) { if (!classMapping.isSupportUnmarshall()) { // we don't support unmarshalling, try and unmarshall just the object with its // ids set. Object obj = constructObjectForUnmarshalling(classMapping, resource, context); for (Mapping id : classMapping.getIdMappings()) { Object idValue = id.getConverter().unmarshall(resource, id, context); if (idValue == null) { // was not marshalled, simply return null return null; } if (((ObjectMapping) id).getSetter() != null) { ((ObjectMapping) id).getSetter().set(obj, idValue); } } return obj; } resourceKey = ((InternalResource) resource).getResourceKey(); // if it is cached, return the cached object Object cached = context.getUnmarshalled(resourceKey); if (cached != null) { return cached; } } else if (classMapping.getIdMappings().length > 0) { // if the class mapping has ids, try and get it from the resource. Property[] propIds = ResourceHelper.toIds(resource, classMapping, false); if (propIds != null) { resourceKey = new ResourceKey(classMapping, propIds); // if it is cached, return the cached object Object cached = context.getUnmarshalled(resourceKey); if (cached != null) { if (resource instanceof CollectionResourceWrapper) { // we read the id, so we need to increment the property counters as well for (Iterator mappingsIt = classMapping.mappingsIt(); mappingsIt.hasNext();) { OsemMapping m = (OsemMapping) mappingsIt.next(); if (!(m instanceof ClassIdPropertyMapping)) { CollectionResourceWrapper colWrapper = (CollectionResourceWrapper) resource; colWrapper.getProperty(m.getName()); } } } return cached; } // if we do have values in the ids, but all of them are null, it means that we // marked a null object boolean nullClass = true; for (Property propId : propIds) { if (!context.getResourceFactory().isNullValue(propId.getStringValue())) { nullClass = false; } } if (nullClass) { return null; } // if it is not cached, we need to rollback the fact that we read // the ids, so the rest of the unmarshalling process will work if (resource instanceof CollectionResourceWrapper) { CollectionResourceWrapper colWrapper = (CollectionResourceWrapper) resource; for (Mapping id : classMapping.getResourceIdMappings()) { colWrapper.rollbackGetProperty(id.getPath().getPath()); } } } } // not root class mapping and does not support unamrhsalling, simply return null if (!classMapping.isSupportUnmarshall()) { return null; } Object obj = constructObjectForUnmarshalling(classMapping, resource, context); context.setAttribute(MarshallingEnvironment.ATTRIBUTE_CURRENT, obj); // we will set here the object, even though no ids have been set, // since the ids are the first mappings that will be unmarshalled, // and it's all we need to handle cyclic refernces in case of // references or components with ids if (resourceKey != null) { context.setUnmarshalled(resourceKey, obj); if (classMapping.isRoot()) { context.getSession().getFirstLevelCache().set(resourceKey, obj); } } boolean isNullClass = true; for (Iterator mappingsIt = classMapping.mappingsIt(); mappingsIt.hasNext();) { context.setAttribute(MarshallingEnvironment.ATTRIBUTE_CURRENT, obj); OsemMapping m = (OsemMapping) mappingsIt.next(); if (m.hasAccessors()) { Setter setter = ((ObjectMapping) m).getSetter(); if (setter == null) { continue; } Object value = m.getConverter().unmarshall(resource, m, context); if (value == null) { continue; } setter.set(obj, value); if (m.controlsObjectNullability()) { isNullClass = false; } } else { m.getConverter().unmarshall(resource, m, context); } } if (isNullClass) { return null; } return obj; } /** * Constructs the object used for unmarshalling (no properties are set/unmarshalled) on it. * <code>null</code> return value denotes no un-marshalling should be performed. */ protected Object constructObjectForUnmarshalling(ClassMapping classMapping, Resource resource, MarshallingContext context) throws ConversionException { // resolve the actual class and constructor Class clazz = classMapping.getClazz(); ReflectionConstructor constructor = classMapping.getConstructor(); if (classMapping.isPoly()) { if (classMapping.getPolyClass() != null) { clazz = classMapping.getPolyClass(); constructor = classMapping.getPolyConstructor(); } else { Property pClassName = resource.getProperty(classMapping.getClassPath().getPath()); if (pClassName == null) { // if not poly class is stored, this means that it is probably a null class stored. return null; } String className = pClassName.getStringValue(); if (className == null) { // if not poly class is stored, this means that it is probably a null class stored. return null; } constructor = cachedConstructors.get(className); if (constructor == null) { try { clazz = ClassUtils.forName(className, context.getSession().getCompass().getSettings().getClassLoader()); } catch (ClassNotFoundException e) { throw new ConversionException("Failed to create class [" + className + "] for unmarshalling", e); } constructor = ReflectionFactory.getDefaultConstructor(context.getSession().getCompass().getSettings(), clazz); cachedConstructors.put(className, constructor); } } } // create the object Object obj; if (clazz.isEnum()) { Property pEnumName = resource.getProperty(classMapping.getEnumNamePath().getPath()); if (pEnumName == null) { return null; } String name = pEnumName.getStringValue(); if (name == null) { return null; } obj = Enum.valueOf(clazz, name); } else { try { obj = constructor.newInstance(); } catch (Exception e) { throw new ConversionException("Failed to create class [" + clazz.getName() + "] for unmarshalling", e); } } return obj; } public boolean marshallIds(Resource idResource, Object id, ResourceMapping resourceMapping, MarshallingContext context) throws ConversionException { ClassMapping classMapping = (ClassMapping) resourceMapping; boolean stored = false; Mapping[] ids = classMapping.getIdMappings(); if (classMapping.getClazz().isAssignableFrom(id.getClass())) { // the object is the key for (Mapping rpId : ids) { ObjectMapping objectMapping = (ObjectMapping) rpId; stored |= convertId(classMapping, idResource, objectMapping.getGetter().get(id), rpId, context); } } else if (id instanceof Object[]) { if (Array.getLength(id) != ids.length) { throw new ConversionException("Trying to load class with [" + Array.getLength(id) + "] mappings while has ids mappings of [" + ids.length + "]"); } for (int i = 0; i < ids.length; i++) { stored |= convertId(classMapping, idResource, Array.get(id, i), ids[i], context); } } else if (ids.length == 1) { stored = convertId(classMapping, idResource, id, ids[0], context); } else { String type = id.getClass().getName(); throw new ConversionException("Cannot marshall ids, not supported id object type [" + type + "] and value [" + id + "], or you have not defined ids in the mapping files"); } if (!context.hasAttribute(DISABLE_UID_MARSHALLING)) { ((InternalResource) idResource).addUID(); } return stored; } private boolean convertId(ResourceMapping resourceMapping, Resource resource, Object root, Mapping mapping, MarshallingContext context) { if (root == null) { throw new ConversionException("Trying to marshall a null id [" + mapping.getName() + "] for alias [" + resourceMapping.getAlias() + "]"); } if ((root instanceof String) && (mapping instanceof ResourcePropertyMapping) && (mapping.getConverter() instanceof ResourcePropertyConverter)) { ResourcePropertyConverter converter = (ResourcePropertyConverter) mapping.getConverter(); if (converter.canNormalize()) { root = converter.fromString((String) root, (ResourcePropertyMapping) mapping); } } return mapping.getConverter().marshall(resource, root, mapping, context); } public Object[] unmarshallIds(Object id, ResourceMapping resourceMapping, MarshallingContext context) throws ConversionException { ClassMapping classMapping = (ClassMapping) resourceMapping; Mapping[] ids = classMapping.getIdMappings(); Object[] idsValues = new Object[ids.length]; if (id instanceof Resource) { Resource resource = (Resource) id; for (int i = 0; i < ids.length; i++) { idsValues[i] = ids[i].getConverter().unmarshall(resource, ids[i], context); if (idsValues[i] == null) { // the reference was not marshalled return null; } } } else if (classMapping.getClazz().isAssignableFrom(id.getClass())) { // the object is the key for (int i = 0; i < ids.length; i++) { ObjectMapping objectMapping = (ObjectMapping) ids[i]; idsValues[i] = objectMapping.getGetter().get(id); } } else if (id instanceof Object[]) { if (Array.getLength(id) != ids.length) { throw new ConversionException("Trying to load class with [" + Array.getLength(id) + "] while has ids mappings of [" + ids.length + "]"); } for (int i = 0; i < ids.length; i++) { idsValues[i] = Array.get(id, i); } } else if (ids.length == 1) { idsValues[0] = id; } else { String type = id.getClass().getName(); throw new ConversionException("Cannot marshall ids, not supported id object type [" + type + "] and value [" + id + "], or you have not defined ids in the mapping files"); } return idsValues; } /** * A simple extension point that allows to set the boost value for the created {@link Resource}. * <p/> * The default implemenation uses the statically defined boost value in the mapping definition * ({@link org.compass.core.mapping.osem.ClassMapping#getBoost()}) to set the boost level * using {@link Resource#setBoost(float)} * <p/> * Note, that this method will only be called on a root level (root=true) mapping. * * @param resource The resource to set the boost on * @param root The Object that is marshalled into the respective Resource * @param classMapping The Class Mapping deifnition * @param context The marshalling context * @throws ConversionException */ protected void doSetBoost(Resource resource, Object root, ClassMapping classMapping, MarshallingContext context) throws ConversionException { resource.setBoost(classMapping.getBoost()); } /** * Stores the poly class name callback. Uses {@link #getPolyClassName(Object)} in order to get * the poly class and store it. */ protected void storePolyClass(Resource resource, Object root, ClassMapping classMapping, MarshallingContext context) { String className = getPolyClassName(root); Property p = context.getResourceFactory().createProperty(classMapping.getClassPath().getPath(), className, Property.Store.YES, Property.Index.NOT_ANALYZED); p.setOmitNorms(true); p.setOmitTf(true); resource.addProperty(p); } /** * Stores the {@link Enum#name()} in order to construct it afterwards. */ protected void storeEnumName(Resource resource, Object root, ClassMapping classMapping, MarshallingContext context) { String name = ((Enum) root).name(); Property p = context.getResourceFactory().createProperty(classMapping.getEnumNamePath().getPath(), name, Property.Store.YES, Property.Index.NOT_ANALYZED); p.setOmitNorms(true); p.setOmitTf(true); resource.addProperty(p); } /** * An extension point allowing to get the poly class name if need to be stored. * By defaults uses {@link org.compass.core.util.proxy.extractor.ProxyExtractorHelper#getTargetClass(Object)}. */ protected String getPolyClassName(Object root) { return proxyExtractorHelper.getTargetClass(root).getName(); } /** * An object key based on the alias and the object identity hash code */ protected static final class IdentityAliasedObjectKey { private String alias; private Integer objHashCode; private Object value; private int hashCode = Integer.MIN_VALUE; public IdentityAliasedObjectKey(String alias, Object value) { this.alias = alias; this.value = value; this.objHashCode = System.identityHashCode(value); } public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof IdentityAliasedObjectKey)) { return false; } final IdentityAliasedObjectKey idObjKey = (IdentityAliasedObjectKey) other; return idObjKey.value == value && idObjKey.alias.equals(this.alias); } public int hashCode() { if (hashCode == Integer.MIN_VALUE) { hashCode = 13 * objHashCode.hashCode() + alias.hashCode(); } return hashCode; } } /** * An object key based on the alias and its ids values */ protected static final class IdsAliasesObjectKey { private String alias; private Object[] idsValues; private boolean hasNullId; private int hashCode = Integer.MIN_VALUE; public IdsAliasesObjectKey(ClassMapping classMapping, Object value) { this.alias = classMapping.getAlias(); Mapping[] ids = classMapping.getIdMappings(); idsValues = new Object[ids.length]; for (int i = 0; i < ids.length; i++) { OsemMapping m = (OsemMapping) ids[i]; if (m.hasAccessors()) { idsValues[i] = ((ObjectMapping) m).getGetter().get(value); } else { idsValues[i] = value; } if (idsValues[i] == null) { hasNullId = true; } } } public Object[] getIdsValues() { return this.idsValues; } public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof IdsAliasesObjectKey)) return false; final IdsAliasesObjectKey key = (IdsAliasesObjectKey) other; if (!key.alias.equals(alias)) { return false; } for (int i = 0; i < idsValues.length; i++) { if (!key.idsValues[i].equals(idsValues[i])) { return false; } } return true; } public int hashCode() { if (hashCode == Integer.MIN_VALUE) { hashCode = getHashCode(); } return hashCode; } private int getHashCode() { int result = alias.hashCode(); for (Object idValue : idsValues) { result = 29 * result + idValue.hashCode(); } return result; } public boolean isHasNullId() { return hasNullId; } } }