001: /*
002: * Copyright 2004-2006 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.compass.core.converter.mapping.osem;
018:
019: import java.lang.reflect.Array;
020: import java.lang.reflect.Constructor;
021: import java.util.Iterator;
022:
023: import org.compass.core.Property;
024: import org.compass.core.Resource;
025: import org.compass.core.accessor.Getter;
026: import org.compass.core.accessor.Setter;
027: import org.compass.core.converter.ConversionException;
028: import org.compass.core.converter.mapping.CollectionResourceWrapper;
029: import org.compass.core.converter.mapping.ResourceMappingConverter;
030: import org.compass.core.engine.utils.ResourceHelper;
031: import org.compass.core.mapping.Mapping;
032: import org.compass.core.mapping.ResourceMapping;
033: import org.compass.core.mapping.osem.ClassMapping;
034: import org.compass.core.mapping.osem.ObjectMapping;
035: import org.compass.core.mapping.osem.OsemMapping;
036: import org.compass.core.marshall.MarshallingContext;
037: import org.compass.core.marshall.MarshallingEnvironment;
038: import org.compass.core.spi.InternalResource;
039: import org.compass.core.spi.ResourceKey;
040: import org.compass.core.util.ClassUtils;
041:
042: /**
043: * @author kimchy
044: */
045: public class ClassMappingConverter implements ResourceMappingConverter {
046:
047: /**
048: * Under this key within the context the root class mapping can be found.
049: */
050: public static final String ROOT_CLASS_MAPPING_KEY = "$rcmk";
051:
052: /**
053: * Disable internal mappings is a marker within the context if internal mappings should not
054: * be marshalled.
055: *
056: * <p>Internal mappings are disabled for inner components (not root classes) when support
057: * unmarshall is set to <code>false</code>.
058: */
059: public static final String DISABLE_INTERNAL_MAPPINGS = "$dim";
060:
061: private static final Object DISABLE_INTERNAL_MAPPINGS_MARK = new Object();
062:
063: public static final String DISABLE_UID_MARSHALLING = "$disableUID";
064:
065: public boolean marshall(Resource resource, Object root,
066: Mapping mapping, MarshallingContext context)
067: throws ConversionException {
068:
069: // first store some important original context
070: Object disableInternalMappings = context
071: .getAttribute(DISABLE_INTERNAL_MAPPINGS);
072:
073: // perform the unmarshalling
074: boolean store = doMarshall(resource, root, mapping, context);
075:
076: // restore the context
077: context.setAttribute(DISABLE_INTERNAL_MAPPINGS,
078: disableInternalMappings);
079:
080: return store;
081: }
082:
083: protected boolean doMarshall(Resource resource, Object root,
084: Mapping mapping, MarshallingContext context)
085: throws ConversionException {
086: ClassMapping classMapping = (ClassMapping) mapping;
087: // Note that even if a component is root, it will not be root when
088: // treated as a component (the binding part of the configuration takes
089: // care and "unroots" it)
090: if (classMapping.isRoot()) {
091: doSetBoost(resource, root, classMapping, context);
092: context.setAttribute(ROOT_CLASS_MAPPING_KEY, classMapping);
093: context.setAttribute(DISABLE_INTERNAL_MAPPINGS, null);
094: } else {
095: if (!classMapping.isSupportUnmarshall()) {
096: context.setAttribute(DISABLE_INTERNAL_MAPPINGS,
097: DISABLE_INTERNAL_MAPPINGS_MARK);
098: }
099: }
100:
101: // handle null values
102: if (root == null) {
103: if (!classMapping.isSupportUnmarshall()) {
104: return false;
105: }
106: if (!context.handleNulls()) {
107: return false;
108: }
109: if (classMapping.getIdMappings().length == 0) {
110: throw new ConversionException(
111: "Component mapping ["
112: + classMapping.getAlias()
113: + "] used within a collection/array and has null value, in such cases please define at least one id mapping on it");
114: }
115: // go over all the ids and put a null value in it (just so we keep the order)
116: boolean store = false;
117: for (Mapping id : classMapping.getResourceIdMappings()) {
118: store |= id.getConverter().marshall(resource,
119: context.getResourceFactory().getNullValue(),
120: id, context);
121: }
122: return store;
123: }
124:
125: if (classMapping.isPoly()
126: && classMapping.getPolyClass() == null) {
127: // store the poly class only for root mappings when we don't support unmarshalling
128: // and for all classes when we do support unmarshalling
129: if (classMapping.isSupportUnmarshall()
130: || classMapping.isRoot()) {
131: storePolyClass(resource, root, classMapping, context);
132: }
133: }
134:
135: // check if we already marshalled this objecy under this alias
136: // if we did, there is no need to completly marhsall it again
137:
138: // When we support *do* unmarshall, it is important that theses will have ids, since based
139: // on them we will unmarshall correctly
140:
141: // When we *do not* support unmarshall, we don't care about ids. Therefore, we can also mark
142: // marshalled based on object identity and support cyclic support for components without ids.
143: if (classMapping.getIdMappings().length > 0) {
144: IdsAliasesObjectKey idObjKey = new IdsAliasesObjectKey(
145: classMapping, root);
146: if (!idObjKey.hasNullId) {
147: Object marshalled = context.getMarshalled(idObjKey);
148: if (marshalled != null) {
149: // we already marshalled this object, if we don't support unmarhsall, just return
150: // otherwise only marshall its ids and return
151: if (!classMapping.isSupportUnmarshall()) {
152: return true;
153: }
154: Mapping[] ids = classMapping.getIdMappings();
155: boolean store = false;
156: for (int i = 0; i < ids.length; i++) {
157: store |= ids[i].getConverter().marshall(
158: resource, idObjKey.idsValues[i],
159: ids[i], context);
160: }
161: return store;
162: } else {
163: // we did not marshall this object, cache it for later checks
164: context.setMarshalled(idObjKey, root);
165: if (!classMapping.isSupportUnmarshall()) {
166: context.setMarshalled(
167: new IdentityAliasedObjectKey(
168: classMapping.getAlias(), root),
169: root);
170: }
171: }
172: }
173: } else if (!classMapping.isSupportUnmarshall()) {
174: IdentityAliasedObjectKey key = new IdentityAliasedObjectKey(
175: classMapping.getAlias(), root);
176: Object marshalled = context.getMarshalled(key);
177: if (marshalled != null) {
178: return true;
179: }
180: context.setMarshalled(key, root);
181: }
182:
183: // perform full marshalling of the object into the resource
184: boolean store = false;
185: for (Iterator mappingsIt = classMapping.mappingsIt(); mappingsIt
186: .hasNext();) {
187: context.setAttribute(
188: MarshallingEnvironment.ATTRIBUTE_CURRENT, root);
189: OsemMapping m = (OsemMapping) mappingsIt.next();
190: Object value;
191: if (m.hasAccessors()) {
192: Getter getter = ((ObjectMapping) m).getGetter();
193: value = getter.get(root);
194: } else {
195: value = root;
196: }
197: store |= m.getConverter().marshall(resource, value, m,
198: context);
199: }
200:
201: // marshall the uid last
202: if (classMapping.isRoot()
203: && !context.hasAttribute(DISABLE_UID_MARSHALLING)) {
204: ((InternalResource) resource).addUID();
205: }
206:
207: return store;
208: }
209:
210: public Object unmarshall(Resource resource, Mapping mapping,
211: MarshallingContext context) throws ConversionException {
212: ClassMapping classMapping = (ClassMapping) mapping;
213: ResourceKey resourceKey = null;
214: // handle a cache of all the unmarshalled objects already, used for
215: // cyclic references when using reference mappings or component mappings
216: // with ids
217: if (classMapping.isRoot()) {
218: if (!classMapping.isSupportUnmarshall()) {
219: // we don't support unmarshalling, try and unmarshall just the object with its
220: // ids set.
221: Object obj = constructObjectForUnmarshalling(
222: classMapping, resource, context);
223: for (Mapping id : classMapping.getIdMappings()) {
224: Object idValue = id.getConverter().unmarshall(
225: resource, id, context);
226: if (idValue == null) {
227: // was not marshalled, simply return null
228: return null;
229: }
230: if (((ObjectMapping) id).getSetter() != null) {
231: ((ObjectMapping) id).getSetter().set(obj,
232: idValue);
233: }
234: }
235: return obj;
236: }
237: resourceKey = ((InternalResource) resource).resourceKey();
238: // if it is cached, return the cached object
239: Object cached = context.getUnmarshalled(resourceKey);
240: if (cached != null) {
241: return cached;
242: }
243: } else if (classMapping.getIdMappings().length > 0) {
244: // if the class mapping has ids, try and get it from the resource.
245: Property[] propIds = ResourceHelper.toIds(resource,
246: classMapping, false);
247: if (propIds != null) {
248: resourceKey = new ResourceKey(classMapping, propIds);
249: // if it is cached, return the cached object
250: Object cached = context.getUnmarshalled(resourceKey);
251: if (cached != null) {
252: return cached;
253: }
254: // if we do have values in the ids, but all of them are null, it means that we
255: // marked a null object
256: boolean nullClass = true;
257: for (Property propId : propIds) {
258: if (!context.getResourceFactory().isNullValue(
259: propId.getStringValue())) {
260: nullClass = false;
261: }
262: }
263: if (nullClass) {
264: return null;
265: }
266: // if it is not cached, we need to rollback the fact that we read
267: // the ids, so the rest of the unmarshalling process will work
268: if (resource instanceof CollectionResourceWrapper) {
269: CollectionResourceWrapper colWrapper = (CollectionResourceWrapper) resource;
270: for (Mapping id : classMapping
271: .getResourceIdMappings()) {
272: colWrapper.rollbackGetProperty(id.getPath()
273: .getPath());
274: }
275: }
276: }
277: }
278:
279: Object obj = constructObjectForUnmarshalling(classMapping,
280: resource, context);
281: context.setAttribute(MarshallingEnvironment.ATTRIBUTE_CURRENT,
282: obj);
283: // we will set here the object, even though no ids have been set,
284: // since the ids are the first mappings that will be unmarshalled,
285: // and it's all we need to handle cyclic refernces in case of
286: // references or components with ids
287: if (resourceKey != null) {
288: context.setUnmarshalled(resourceKey, obj);
289: if (classMapping.isRoot()) {
290: context.getSession().getFirstLevelCache().set(
291: resourceKey, obj);
292: }
293: }
294:
295: boolean isNullClass = true;
296: for (Iterator mappingsIt = classMapping.mappingsIt(); mappingsIt
297: .hasNext();) {
298: context.setAttribute(
299: MarshallingEnvironment.ATTRIBUTE_CURRENT, obj);
300: OsemMapping m = (OsemMapping) mappingsIt.next();
301: if (m.hasAccessors()) {
302: Setter setter = ((ObjectMapping) m).getSetter();
303: if (setter == null) {
304: continue;
305: }
306: Object value = m.getConverter().unmarshall(resource, m,
307: context);
308: if (value == null) {
309: continue;
310: }
311: setter.set(obj, value);
312: if (m.controlsObjectNullability()) {
313: isNullClass = false;
314: }
315: } else {
316: m.getConverter().unmarshall(resource, m, context);
317: }
318: }
319: if (isNullClass) {
320: return null;
321: }
322: return obj;
323: }
324:
325: /**
326: * Constructs the object used for unmarshalling (no properties are set/unmarshalled) on it.
327: * <code>null</code> return value denotes no un-marshalling should be performed.
328: */
329: protected Object constructObjectForUnmarshalling(
330: ClassMapping classMapping, Resource resource,
331: MarshallingContext context) throws ConversionException {
332: // resolve the actual class and constructor
333: Class clazz = classMapping.getClazz();
334: Constructor constructor = classMapping.getConstructor();
335: if (classMapping.isPoly()) {
336: if (classMapping.getPolyClass() != null) {
337: clazz = classMapping.getPolyClass();
338: constructor = classMapping.getPolyConstructor();
339: } else {
340: Property pClassName = resource.getProperty(classMapping
341: .getClassPath().getPath());
342: if (pClassName == null) {
343: // if not poly class is stored, this means that it is probably a null class stored.
344: return null;
345: }
346: String className = pClassName.getStringValue();
347: if (className == null) {
348: // if not poly class is stored, this means that it is probably a null class stored.
349: return null;
350: }
351: try {
352: clazz = ClassUtils.forName(className, context
353: .getSession().getCompass().getSettings()
354: .getClassLoader());
355: } catch (ClassNotFoundException e) {
356: throw new ConversionException(
357: "Failed to create class [" + className
358: + "] for unmarshalling", e);
359: }
360: constructor = ClassUtils.getDefaultConstructor(clazz);
361: }
362: }
363:
364: // create the object
365: Object obj;
366: try {
367: obj = constructor.newInstance();
368: } catch (Exception e) {
369: throw new ConversionException("Failed to create class ["
370: + clazz.getName() + "] for unmarshalling", e);
371: }
372: return obj;
373: }
374:
375: public boolean marshallIds(Resource idResource, Object id,
376: ResourceMapping resourceMapping, MarshallingContext context)
377: throws ConversionException {
378: ClassMapping classMapping = (ClassMapping) resourceMapping;
379: boolean stored = false;
380: Mapping[] ids = classMapping.getIdMappings();
381: if (classMapping.getClazz().isAssignableFrom(id.getClass())) {
382: // the object is the key
383: for (Mapping rpId : ids) {
384: ObjectMapping objectMapping = (ObjectMapping) rpId;
385: stored |= convertId(idResource, objectMapping
386: .getGetter().get(id), rpId, context);
387: }
388: } else if (id instanceof Object[]) {
389: if (Array.getLength(id) != ids.length) {
390: throw new ConversionException(
391: "Trying to load class with ["
392: + Array.getLength(id)
393: + "] mappings while has ids mappings of ["
394: + ids.length + "]");
395: }
396: for (int i = 0; i < ids.length; i++) {
397: stored |= convertId(idResource, Array.get(id, i),
398: ids[i], context);
399: }
400: } else if (ids.length == 1) {
401: stored = convertId(idResource, id, ids[0], context);
402: } else {
403: String type = id.getClass().getName();
404: throw new ConversionException(
405: "Cannot marshall ids, not supported id object type ["
406: + type
407: + "] and value ["
408: + id
409: + "], or you have not defined ids in the mapping files");
410: }
411:
412: if (!context.hasAttribute(DISABLE_UID_MARSHALLING)) {
413: ((InternalResource) idResource).addUID();
414: }
415:
416: return stored;
417: }
418:
419: private boolean convertId(Resource resource, Object root,
420: Mapping mapping, MarshallingContext context) {
421: if (root == null) {
422: throw new ConversionException(
423: "Trying to marshall a null id ["
424: + mapping.getName() + "]");
425: }
426: return mapping.getConverter().marshall(resource, root, mapping,
427: context);
428: }
429:
430: public Object[] unmarshallIds(Object id,
431: ResourceMapping resourceMapping, MarshallingContext context)
432: throws ConversionException {
433: ClassMapping classMapping = (ClassMapping) resourceMapping;
434: Mapping[] ids = classMapping.getIdMappings();
435: Object[] idsValues = new Object[ids.length];
436: if (id instanceof Resource) {
437: Resource resource = (Resource) id;
438: for (int i = 0; i < ids.length; i++) {
439: idsValues[i] = ids[i].getConverter().unmarshall(
440: resource, ids[i], context);
441: if (idsValues[i] == null) {
442: // the reference was not marshalled
443: return null;
444: }
445: }
446: } else if (classMapping.getClazz().isAssignableFrom(
447: id.getClass())) {
448: // the object is the key
449: for (int i = 0; i < ids.length; i++) {
450: ObjectMapping objectMapping = (ObjectMapping) ids[i];
451: idsValues[i] = objectMapping.getGetter().get(id);
452: }
453: } else if (id instanceof Object[]) {
454: if (Array.getLength(id) != ids.length) {
455: throw new ConversionException(
456: "Trying to load class with ["
457: + Array.getLength(id)
458: + "] while has ids mappings of ["
459: + ids.length + "]");
460: }
461: for (int i = 0; i < ids.length; i++) {
462: idsValues[i] = Array.get(id, i);
463: }
464: } else if (ids.length == 1) {
465: idsValues[0] = id;
466: } else {
467: String type = id.getClass().getName();
468: throw new ConversionException(
469: "Cannot marshall ids, not supported id object type ["
470: + type
471: + "] and value ["
472: + id
473: + "], or you have not defined ids in the mapping files");
474: }
475: return idsValues;
476: }
477:
478: /**
479: * A simple extension point that allows to set the boost value for the created {@link Resource}.
480: * <p/>
481: * The default implemenation uses the statically defined boost value in the mapping definition
482: * ({@link org.compass.core.mapping.osem.ClassMapping#getBoost()}) to set the boost level
483: * using {@link Resource#setBoost(float)}
484: * <p/>
485: * Note, that this method will only be called on a root level (root=true) mapping.
486: *
487: * @param resource The resource to set the boost on
488: * @param root The Object that is marshalled into the respective Resource
489: * @param classMapping The Class Mapping deifnition
490: * @param context The marshalling context
491: * @throws ConversionException
492: */
493: protected void doSetBoost(Resource resource, Object root,
494: ClassMapping classMapping, MarshallingContext context)
495: throws ConversionException {
496: resource.setBoost(classMapping.getBoost());
497: }
498:
499: /**
500: * Stores the poly class name callback. Uses {@link #getPolyClassName(Object)} in order to get
501: * the poly class and store it.
502: */
503: protected void storePolyClass(Resource resource, Object root,
504: ClassMapping classMapping, MarshallingContext context) {
505: String className = getPolyClassName(root);
506: Property p = context.getResourceFactory().createProperty(
507: classMapping.getClassPath().getPath(), className,
508: Property.Store.YES, Property.Index.UN_TOKENIZED);
509: p.setOmitNorms(true);
510: resource.addProperty(p);
511: }
512:
513: /**
514: * An extension point allowing to get the poly class name if need to be stored.
515: * By defaults uses {@link Class#getName()}.
516: */
517: protected String getPolyClassName(Object root) {
518: return root.getClass().getName();
519: }
520:
521: /**
522: * An object key based on the alias and the object identity hash code
523: */
524: protected static final class IdentityAliasedObjectKey {
525:
526: private String alias;
527:
528: private Integer objHashCode;
529:
530: private int hashCode = Integer.MIN_VALUE;
531:
532: public IdentityAliasedObjectKey(String alias, Object value) {
533: this .alias = alias;
534: this .objHashCode = System.identityHashCode(value);
535: }
536:
537: public boolean equals(Object other) {
538: if (this == other) {
539: return true;
540: }
541: final IdentityAliasedObjectKey idObjKey = (IdentityAliasedObjectKey) other;
542: return idObjKey.objHashCode.equals(this .objHashCode)
543: && idObjKey.alias.equals(this .alias);
544: }
545:
546: public int hashCode() {
547: if (hashCode == Integer.MIN_VALUE) {
548: hashCode = 13 * objHashCode.hashCode()
549: + alias.hashCode();
550: }
551: return hashCode;
552: }
553: }
554:
555: /**
556: * An object key based on the alias and its ids values
557: */
558: protected static final class IdsAliasesObjectKey {
559:
560: private String alias;
561:
562: private Object[] idsValues;
563:
564: private boolean hasNullId;
565:
566: private int hashCode = Integer.MIN_VALUE;
567:
568: public IdsAliasesObjectKey(ClassMapping classMapping,
569: Object value) {
570: this .alias = classMapping.getAlias();
571: Mapping[] ids = classMapping.getIdMappings();
572: idsValues = new Object[ids.length];
573: for (int i = 0; i < ids.length; i++) {
574: OsemMapping m = (OsemMapping) ids[i];
575: if (m.hasAccessors()) {
576: idsValues[i] = ((ObjectMapping) m).getGetter().get(
577: value);
578: } else {
579: idsValues[i] = value;
580: }
581: if (idsValues[i] == null) {
582: hasNullId = true;
583: }
584: }
585: }
586:
587: public Object[] getIdsValues() {
588: return this .idsValues;
589: }
590:
591: public boolean equals(Object other) {
592: if (this == other)
593: return true;
594:
595: // We will make sure that it never happens
596: // if (!(other instanceof IdsAliasesObjectKey))
597: // return false;
598:
599: final IdsAliasesObjectKey key = (IdsAliasesObjectKey) other;
600: if (!key.alias.equals(alias)) {
601: return false;
602: }
603:
604: for (int i = 0; i < idsValues.length; i++) {
605: if (!key.idsValues[i].equals(idsValues[i])) {
606: return false;
607: }
608: }
609:
610: return true;
611: }
612:
613: public int hashCode() {
614: if (hashCode == Integer.MIN_VALUE) {
615: hashCode = getHashCode();
616: }
617: return hashCode;
618: }
619:
620: private int getHashCode() {
621: int result = alias.hashCode();
622: for (Object idValue : idsValues) {
623: result = 29 * result + idValue.hashCode();
624: }
625: return result;
626: }
627:
628: public boolean isHasNullId() {
629: return hasNullId;
630: }
631: }
632: }
|