org.eclipselabs.mongoemf.builders.EObjectBuilderImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipselabs.mongoemf.builders.EObjectBuilderImpl.java

Source

/*******************************************************************************
 * Copyright (c) 2012 Bryan Hunt & Ed Merks.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bryan Hunt & Ed Merks - initial API and implementation
 *******************************************************************************/

package org.eclipselabs.mongoemf.builders;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.bson.types.ObjectId;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipselabs.mongoemf.ConverterService;
import org.eclipselabs.mongoemf.EObjectBuilder;
import org.eclipselabs.mongoemf.Keywords;
import org.eclipselabs.mongoemf.MongoUtils;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;

/**
 * This class builds EMF EObjects from a MongoDB DBObject. This builder is designed to be extensible,
 * but you must be aware of the assumptions made by each of the functions in the builder.
 * 
 * This class is thread safe.
 * 
 * @author bhunt
 */
public class EObjectBuilderImpl implements EObjectBuilder {
    /**
     * Constructs an object builder without an EClass cache.
     * 
     * @param converterService the service to use when converting attribute values
     * @param uriHandler the handler for creating proxy URIs
     * @param includeAttributesForProxyReferences true if you want attribute values to be set on proxy references; false otherwise
     */
    public EObjectBuilderImpl(ConverterService converterService, XMLResource.URIHandler uriHandler,
            boolean includeAttributesForProxyReferences) {
        this(converterService, uriHandler, includeAttributesForProxyReferences, null);
    }

    /**
     * Constructs an object builder with an optional EClass cache.
     * 
     * @param converterService the service to use when converting attribute values
     * @param uriHandler the handler for creating proxy URIs
     * @param includeAttributesForProxyReferences true if you want attribute values to be set on proxy references; false otherwise
     * @param eClassCache the cache to use to EClass lookups when building the EObject instance - may be null
     */
    public EObjectBuilderImpl(ConverterService converterService, XMLResource.URIHandler uriHandler,
            boolean includeAttributesForProxyReferences, Map<String, EClass> eClassCache) {
        this.converterService = converterService;
        this.uriHandler = uriHandler;
        this.includeAttributesForProxyReferences = includeAttributesForProxyReferences;
        this.eClassCache = eClassCache;
    }

    /**
     * Build an EMF EObject from the supplied DBObject from MongoDB.
     * 
     * @param collection the MongoDB collection containing the DBObject
     * @param dbObject the object read from MongoDB
     * @param resource the resource that will contain the EMF Object
     *          This function will not add the created to the supplied resource
     *          to support the case where the user wishes to return a collection
     *          of objects such as the result of a query.
     * @param isProxy true if the object is to be built as a proxy; false otherwise
     * @return the newly created EMF object instance
     */
    @Override
    public EObject buildEObject(DBCollection collection, DBObject dbObject, Resource resource, boolean isProxy) {
        // Build an empty EMF object to hold the data from the MongodDB object
        // This function should not add the object to the resource since the
        // object may become part of a collection returned in the resource such
        // as the result of a query.

        EObject eObject = createEObject(resource.getResourceSet(), dbObject);
        EClass eClass = eObject.eClass();

        // Load the XML extrinsic id if necessary

        buildExtransicID(dbObject, resource, eObject);

        // All attributes are mapped as key / value pairs with the key being the attribute name.

        for (EAttribute attribute : eClass.getEAllAttributes()) {
            if (!isProxy || !FeatureMapUtil.isFeatureMap(attribute))
                buildAttribute(collection, dbObject, resource, eObject, attribute);
        }

        // isProxy will be set to true when the object is being returned as
        // part of a collection such as the result of a query.

        if (isProxy) {
            URI proxyURI = URI.createURI("../" + collection.getName() + "/" + dbObject.get(Keywords.ID_KEY) + "#/");
            ((InternalEObject) eObject).eSetProxyURI(uriHandler.resolve(proxyURI));
            return eObject;
        }

        // All references are mapped as key / value pairs with the key being the reference name.

        for (EReference reference : eClass.getEAllReferences())
            buildReference(collection, dbObject, resource, eObject, reference);

        return eObject;
    }

    @Override
    public URI buildURI(DBCollection collection, DBObject object) {
        URI uri = URI.createURI("../" + collection.getName() + "/" + object.get(Keywords.ID_KEY));
        return uriHandler.resolve(uri);
    }

    /**
     * Builds an attribute value from the DBObject, converting the value if necessary.
     * Feature maps are delegated to buildFeatureMap() and non-native arrays to
     * buildAttributeArray(). The converter service is used for value conversion.
     * Attribute values are expected to be mapped in the DBObject using the attribute
     * name as the key.
     * 
     * @param collection the MongoDB collection containing the DBObject
     * @param dbObject the object read from MongoDB
     * @param resource the resource that will contain the EMF Object
     * @param eObject the EMF object being built
     * @param attribute the attribute to set on the EMF object
     */
    @SuppressWarnings("unchecked")
    protected void buildAttribute(DBCollection collection, DBObject dbObject, Resource resource, EObject eObject,
            EAttribute attribute) {
        // Attributes are mapped as key / value pairs with the key being the attribute name.

        if (!attribute.isTransient() && dbObject.containsField(attribute.getName())) {
            Object value = dbObject.get(attribute.getName());

            if (FeatureMapUtil.isFeatureMap(attribute))
                buildFeatureMap(collection, resource, eObject, attribute, (List<DBObject>) value);
            else if (attribute.isMany())
                buildAttributeArray(eObject, attribute, value);
            else
                buildAttributeValue(eObject, attribute, value);
        }
    }

    /**
     * Builds a multi-value attribute from a collection of values. Each value is
     * converted using the converter service.
     * 
     * @param eObject the EMF object being built
     * @param attribute the attribute to set on the EMF object
     * @param values the raw, unconverted, collection of values of the attribute
     */
    protected void buildAttributeArray(EObject eObject, EAttribute attribute, Object values) {
        if (!MongoUtils.isNativeType(attribute.getEAttributeType())) {
            @SuppressWarnings("unchecked")
            List<Object> eValues = (List<Object>) values;
            ArrayList<Object> convertedValues = new ArrayList<Object>(eValues.size());
            EDataType eDataType = attribute.getEAttributeType();

            for (Object dbValue : eValues)
                convertedValues.add(convertMongoDBValueToEMFValue(eDataType, dbValue));

            eObject.eSet(attribute, convertedValues);
        } else
            eObject.eSet(attribute, values);
    }

    /**
     * Sets the attribute value on the EMF object after converting it using the converter service.
     * 
     * @param eObject the EMF object being built
     * @param attribute the attribute to set on the EMF object
     * @param value the value read from MongoDB
     */
    protected void buildAttributeValue(EObject eObject, EAttribute attribute, Object value) {
        EDataType eDataType = attribute.getEAttributeType();
        eObject.eSet(attribute, convertMongoDBValueToEMFValue(eDataType, value));
    }

    /**
     * Sets the extrensic ID if it exists and the resource is of type XMLResource. The
     * extrensic ID is expected to be mapped to the key EXTRINSIC_ID_KEY.
     * 
     * @param dbObject the object read from MongoDB
     * @param resource the resource that will contain the EMF Object
     * @param eObject the EMF object being built
     */
    protected void buildExtransicID(DBObject dbObject, Resource resource, EObject eObject) {
        String id = (String) dbObject.get(Keywords.EXTRINSIC_ID_KEY);

        if (id != null && resource instanceof XMLResource)
            ((XMLResource) resource).setID(eObject, id);
    }

    /**
     * Builds a feature map from the attribute value. Feature maps
     * of references are delegated to buildReferencedObject to build
     * the referenced object.
     * 
     * @param collection the MongoDB collection containing the DBObject
     * @param resource the resource that will contain the EMF Object
     * @param eObject the EMF object being built
     * @param attribute the attribute to set on the EMF object
     * @param values the values extracted from the database as a list
     */
    protected void buildFeatureMap(DBCollection collection, Resource resource, EObject eObject,
            EAttribute attribute, List<DBObject> values) {
        FeatureMap.Internal featureMap = (FeatureMap.Internal) eObject.eGet(attribute);

        for (DBObject entry : values) {
            EStructuralFeature feature = (EStructuralFeature) resource.getResourceSet()
                    .getEObject(URI.createURI((String) entry.get("key")), true);

            if (feature instanceof EAttribute) {
                EDataType eDataType = ((EAttribute) feature).getEAttributeType();
                featureMap.add(feature, convertMongoDBValueToEMFValue(eDataType, entry.get("value")));
            } else {
                EReference reference = (EReference) feature;
                DBObject dbReference = (DBObject) entry.get("value");
                EObject target = buildReferencedObject(collection, dbReference, resource,
                        reference.isResolveProxies());
                featureMap.add(feature, target);
            }
        }
    }

    /**
     * Builds a reference value from the DBObject. References with cardinality greater
     * than one are expected to be stored as a java.util.List of DBObject. References
     * with cardinality equal to one are expected to be stored as a DBObject.
     * Reference values are expected to be mapped in the DBObject using the reference
     * name as the key. Building of the referenced object is delegated to
     * buildReferencedObject().
     * 
     * @param collection the MongoDB collection containing the DBObject
     * @param dbObject the object read from MongoDB
     * @param resource the resource that will contain the EMF Object
     * @param eObject the EMF object being built
     * @param reference the reference to set on the EMF object
     */
    protected void buildReference(DBCollection collection, DBObject dbObject, Resource resource, EObject eObject,
            EReference reference) {
        // References are mapped as key / value pairs with the key being the reference name.

        if (!reference.isTransient() && dbObject.containsField(reference.getName())) {
            boolean isResolveProxies = reference.isResolveProxies();

            if (reference.isMany()) {
                // One to many reference

                @SuppressWarnings("unchecked")
                List<DBObject> dbReferences = (List<DBObject>) dbObject.get(reference.getName());

                @SuppressWarnings("unchecked")
                EList<EObject> eObjects = (EList<EObject>) eObject.eGet(reference);

                for (DBObject dbReference : dbReferences) {
                    EObject target = buildReferencedObject(collection, dbReference, resource, isResolveProxies);
                    eObjects.add(target);
                }
            } else {
                // One to one reference

                DBObject dbReference = (DBObject) dbObject.get(reference.getName());
                EObject target = buildReferencedObject(collection, dbReference, resource, isResolveProxies);
                eObject.eSet(reference, target);
            }
        }
    }

    /**
     * Builds the EObject for a reference. The EObject may be a proxy. The reference
     * value is assumed to be stored in the DBObject as { PROXY_KEY : String } where the
     * string is an EMF proxy URI. If the proxy URI is not null, the proxy EObject is
     * built by delegating to buildProxy(). If the proxy URI is null, the referenced
     * object is treated as an embedded reference and is built by delegating to buildEObject().
     * 
     * @param collection the MongoDB collection containing the DBObject
     * @param dbReference the DBObject containing the referenced object / proxy
     * @param resource the resource that will contain the EMF Object
     * @param referenceResolvesProxies true if the references resolves proxies; false otherwise
     * @return
     */
    protected EObject buildReferencedObject(DBCollection collection, DBObject dbReference, Resource resource,
            boolean referenceResolvesProxies) {
        if (dbReference == null)
            return null;

        // Build an EMF reference from the data in MongoDB.

        String proxy = (String) dbReference.get(Keywords.PROXY_KEY);

        if (proxy == null)
            return buildEObject(collection, dbReference, resource, false);
        else
            return buildProxy(collection, dbReference, resource.getResourceSet(), referenceResolvesProxies);
    }

    /**
     * Builds an EMF proxy object from the reference DBObject
     * 
     * @param collection the collection containing the referencing object
     * @param dbReference the MongoDB reference - must be of the form { ECLASS_KEY : eClassURI, PROXY_KEY : proxyURI }
     * @param resourceSet the resource set to use for building the proxy
     * @param referenceResolvesProxies true if the reference resolves proxies; false otherwise
     * @return the proxy object when referenceResolvedProxies is true, the resolved object otherwise
     */
    protected EObject buildProxy(DBCollection collection, DBObject dbReference, ResourceSet resourceSet,
            boolean referenceResolvesProxies) {
        EObject eObject;
        URI proxyURI = URI.createURI((String) dbReference.get(Keywords.PROXY_KEY));
        URI resolvedProxyURI = uriHandler.resolve(proxyURI);

        if (!referenceResolvesProxies) {
            // When referenceResolvedProxies is false, we must resolve the proxy in place and get the referenced object

            eObject = resourceSet.getEObject(resolvedProxyURI, true);
        } else {
            eObject = createEObject(resourceSet, dbReference);

            ((InternalEObject) eObject).eSetProxyURI(resolvedProxyURI);

            if (includeAttributesForProxyReferences && proxyURI.isRelative() && "/".equals(proxyURI.fragment())) {
                DBCollection referenceCollection = null;

                if (proxyURI.segmentCount() == 3 && proxyURI.segment(0).equals("..")) {
                    referenceCollection = collection.getDB().getCollection(proxyURI.segment(1));
                } else if (proxyURI.segmentCount() == 1) {
                    referenceCollection = collection;
                }

                if (referenceCollection != null) {
                    DBObject referenceDBObject = new BasicDBObject(Keywords.ID_KEY,
                            new ObjectId(proxyURI.lastSegment()));
                    DBObject referencedDBObject = referenceCollection.findOne(referenceDBObject);

                    if (referencedDBObject != null) {
                        for (EAttribute attribute : eObject.eClass().getEAllAttributes()) {
                            if (!attribute.isTransient() && !FeatureMapUtil.isFeatureMap(attribute))
                                buildAttribute(referenceCollection, referencedDBObject, null, eObject, attribute);
                        }
                    }
                }
            }
        }

        return eObject;
    }

    /**
     * Converts the MongoDB value into an EMF value using the converter service
     * 
     * @param eDataType the value type
     * @param dbValue the value
     * @return the converted value
     */
    protected Object convertMongoDBValueToEMFValue(EDataType eDataType, Object dbValue) {
        Object convertedValue = dbValue;

        if (!MongoUtils.isNativeType(eDataType)) {
            // Types not native to MongoDB are stored as strings and must be converted to the proper object type by EMF

            convertedValue = converterService.getConverter(eDataType).convertMongoDBValueToEMFValue(eDataType,
                    dbValue);
        } else if (dbValue != null) {
            // If the type is a byte, float, or short, it must be converted from the native MongoDB type
            // It is valid to use == for string comparison in this case.

            String instanceClassName = eDataType.getInstanceClassName();

            if (instanceClassName == "byte" || instanceClassName == "java.lang.Byte")
                convertedValue = ((Integer) dbValue).byteValue();
            else if (instanceClassName == "float" || instanceClassName == "java.lang.Float")
                convertedValue = ((Double) dbValue).floatValue();
            else if (instanceClassName == "short" || instanceClassName == "java.lang.Short")
                convertedValue = ((Integer) dbValue).shortValue();
        }

        return convertedValue;
    }

    /**
     * This function creates an empty EObject by extracting the EClass type from the given DBObject
     * using the ECLASS_KEY. This function also maintains a static cache of EClass URI to EClass for
     * improved performance.
     * 
     * @param resourceSet the resourceSet that will be used to locate the EClass if it is not cached
     * @param dbObject the object read from MongoDB
     * @return the newly created object of type as specified by the data read from MongoDB
     */
    protected EObject createEObject(ResourceSet resourceSet, DBObject dbObject) {
        String eClassURI = (String) dbObject.get(Keywords.ECLASS_KEY);
        EClass eClass = getEClass(resourceSet, eClassURI);
        return EcoreUtil.create(eClass);
    }

    /**
     * Finds the EClass for the given URI
     * 
     * @param resourceSet the resource set used to locate the EClass if it was not
     *          found in the cache
     * @param eClassURI the URI of the EClass
     * @return the EClass instance for the given URI
     */
    protected EClass getEClass(ResourceSet resourceSet, String eClassURI) {
        if (eClassCache != null) {
            synchronized (eClassCache) {
                EClass eClass = eClassCache.get(eClassURI);

                if (eClass == null) {
                    eClass = (EClass) resourceSet.getEObject(URI.createURI(eClassURI), true);
                    eClassCache.put(eClassURI, eClass);
                }

                return eClass;
            }
        }

        return (EClass) resourceSet.getEObject(URI.createURI(eClassURI), true);
    }

    private ConverterService converterService;
    private XMLResource.URIHandler uriHandler;
    private Map<String, EClass> eClassCache;
    private boolean includeAttributesForProxyReferences;
}