/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.common.instance.orient; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import javax.xml.namespace.QName; import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal; import com.orientechnologies.orient.core.db.record.ODatabaseRecord; import com.orientechnologies.orient.core.metadata.schema.OSchema; import com.orientechnologies.orient.core.metadata.schema.OType; import com.orientechnologies.orient.core.record.ORecordAbstract; import com.orientechnologies.orient.core.record.impl.ODocument; import com.orientechnologies.orient.core.storage.OStorage.CLUSTER_TYPE; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.instance.model.Group; import eu.esdihumboldt.hale.common.instance.model.Instance; import eu.esdihumboldt.hale.common.instance.model.MutableGroup; import eu.esdihumboldt.hale.common.instance.orient.internal.ONamespaceMap; import eu.esdihumboldt.hale.common.instance.orient.internal.OSerializationHelper; import eu.esdihumboldt.hale.common.schema.model.ChildDefinition; import eu.esdihumboldt.hale.common.schema.model.Definition; import eu.esdihumboldt.hale.common.schema.model.DefinitionGroup; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.type.HasValueFlag; /** * Group implementation based on {@link ODocument}s * * @author Simon Templer * @partner 01 / Fraunhofer Institute for Computer Graphics Research */ public class OGroup implements MutableGroup { private static final ALogger log = ALoggerFactory.getLogger(OGroup.class); /** * The set of special field names, e.g. for the binary wrapper field */ private static final Set<String> SPECIAL_FIELDS = new HashSet<String>(); static { SPECIAL_FIELDS.add(OSerializationHelper.BINARY_WRAPPER_FIELD); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_SERIALIZATION_TYPE); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_CONVERT_ID); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_CRS_ID); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_STRING_VALUE); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_COLLECTION_TYPE); SPECIAL_FIELDS.add(OSerializationHelper.FIELD_VALUES); } /** * The document backing the group */ protected final ODocument document; /** * The associated database record. */ protected ODatabaseRecord db; /** * The definition group */ private final DefinitionGroup definition; /** * Creates an empty group with an associated definition group. * * @param definition the associated group */ public OGroup(DefinitionGroup definition) { document = new ODocument(); this.definition = definition; } /** * Configure the internal document with the given database and return it * * @param db the database * @return the internal document configured with the database */ public ODocument configureDocument(ODatabaseRecord db) { ODatabaseRecordThreadLocal.INSTANCE.set(db); configureDocument(document, db, definition); return document; } /** * Get the internal document. * * @return the internal document */ public ODocument getDocument() { return document; } private void configureDocument(ORecordAbstract<?> document, ODatabaseRecord db, DefinitionGroup definition) { // configure document // as of OrientDB 1.0rc8 the database may no longer be set on the // document // instead the current database can be set using // ODatabaseRecordThreadLocal.INSTANCE.set(db); // document.setDatabase(db); if (document instanceof ODocument) { // reset class name ODocument doc = (ODocument) document; /* * Attention: Two long class names cause problems as file names will * be based on them. */ String className = null; if (definition != null) { className = ONamespaceMap.encode(determineName(definition)); } else if (doc.containsField(OSerializationHelper.BINARY_WRAPPER_FIELD) || doc.containsField(OSerializationHelper.FIELD_SERIALIZATION_TYPE)) { className = OSerializationHelper.BINARY_WRAPPER_CLASSNAME; } if (className != null) { OSchema schema = db.getMetadata().getSchema(); if (!schema.existsClass(className)) { // if the class doesn't exist yet, create a physical cluster // manually for it int cluster = db.addCluster(className, CLUSTER_TYPE.PHYSICAL); schema.createClass(className, cluster); } doc.setClassName(className); } // configure children for (Entry<String, Object> field : doc) { List<ODocument> docs = new ArrayList<ODocument>(); List<ORecordAbstract<?>> recs = new ArrayList<ORecordAbstract<?>>(); if (field.getValue() instanceof Collection<?>) { for (Object value : (Collection<?>) field.getValue()) { if (value instanceof ODocument && !getSpecialFieldNames().contains(field.getKey())) { docs.add((ODocument) value); } else if (value instanceof ORecordAbstract<?>) { recs.add((ORecordAbstract<?>) value); } } } else if (field.getValue() instanceof ODocument && !getSpecialFieldNames().contains(field.getKey())) { docs.add((ODocument) field.getValue()); } else if (field.getValue() instanceof ORecordAbstract<?>) { recs.add((ORecordAbstract<?>) field.getValue()); } if (definition != null) { for (ODocument valueDoc : docs) { ChildDefinition<?> child = definition.getChild(decodeProperty(field .getKey())); DefinitionGroup childGroup; if (child.asProperty() != null) { childGroup = child.asProperty().getPropertyType(); } else if (child.asGroup() != null) { childGroup = child.asGroup(); } else { throw new IllegalStateException( "Document is associated neither with a property nor a property group."); } configureDocument(valueDoc, db, childGroup); } } for (ORecordAbstract<?> fieldRec : recs) { configureDocument(fieldRec, db, null); } } } } /** * Determine the name to use for a definition group as class name to encode. * * @param definition the definition group * @return the name to encode as class name */ private static QName determineName(DefinitionGroup definition) { if (definition instanceof Definition) { return ((Definition<?>) definition).getName(); } return new QName(definition.getIdentifier()); } /** * Creates a group based on the given document * * @param document the document * @param definition the definition of the associated group * @param db the database */ public OGroup(ODocument document, DefinitionGroup definition, ODatabaseRecord db) { this.document = document; this.definition = definition; this.db = db; } /** * Copy constructor. Creates a group based on the properties and values of * the given group. * * @param org the instance to copy */ public OGroup(Group org) { this(org.getDefinition()); for (QName property : org.getPropertyNames()) { setProperty(property, org.getProperty(property).clone()); } } /** * @see MutableGroup#addProperty(QName, Object) */ @Override public void addProperty(QName propertyName, Object value) { addProperty(propertyName, value, document); } /** * Adds a property value to a given {@link ODocument} * * @param propertyName the property name * @param value the property value * @param document the {link ODocument} where the value is to add */ @SuppressWarnings("unchecked") protected void addProperty(QName propertyName, Object value, ODocument document) { boolean isInstanceDocument = document == this.document; // convert instances to documents value = convertInstance(value); String pName = encodeProperty(propertyName); boolean collection = !isInstanceDocument || isCollectionProperty(propertyName); if (collection) { // combine value with previous ones Object oldValue = document.field(pName); if (oldValue == null) { // default: use list List<Object> valueList = new ArrayList<Object>(); valueList.add(value); document.field(pName, valueList, (isInstanceDocument) ? (getCollectionType(propertyName)) : (OType.EMBEDDEDLIST)); } else if (oldValue instanceof Collection<?>) { // add value to collection ((Collection<Object>) oldValue).add(value); } else if (oldValue.getClass().isArray()) { // create new array Object[] oldArray = (Object[]) oldValue; Object[] values = new Object[oldArray.length + 1]; System.arraycopy(oldArray, 0, values, 0, oldArray.length); values[oldArray.length] = value; document.field(pName, values, (isInstanceDocument) ? (getCollectionType(propertyName)) : (OType.EMBEDDEDLIST)); } } else { // just set the field document.field(pName, value); } } /** * Get the OrientDB collection type for the given property name * * @param propertyName the property name * @return the collection type, either {@link OType#EMBEDDEDLIST} or * {@link OType#LINKLIST} */ private OType getCollectionType(QName propertyName) { ChildDefinition<?> child = definition.getChild(propertyName); if (child != null) { if (child.asProperty() != null) { TypeDefinition propType = child.asProperty().getPropertyType(); if (propType.getConstraint(HasValueFlag.class).isEnabled()) { return OType.EMBEDDEDLIST; } else { return OType.LINKLIST; } } else if (child.asGroup() != null) { // values must be OGroups return OType.LINKLIST; } } // default to embedded llist return OType.EMBEDDEDLIST; } /** * Converts {@link Group}s and {@link Instance}s to {@link ODocument} but * leaves other objects untouched. * * @param value the object to convert * @return the converted object */ protected Object convertInstance(Object value) { return OSerializationHelper.convertForDB(value); } /** * Determines if a property can have multiple values * * @param propertyName the property name * @return if the property can have multiple values */ private boolean isCollectionProperty(QName propertyName) { // ChildDefinition<?> child = definition.getChild(propertyName); // long max; // if (child instanceof PropertyDefinition) { // max = ((PropertyDefinition) child).getConstraint(Cardinality.class).getMaxOccurs(); // } // else if (child instanceof GroupPropertyDefinition) { // max = ((GroupPropertyDefinition) child).getConstraint(Cardinality.class).getMaxOccurs(); // } // else { // // default to true // return true; // } // // return max == Cardinality.UNBOUNDED || max > 1; // XXX treat everything as a collection property, as we may deal with // merged instances return true; } /** * @see MutableGroup#setProperty(QName, Object[]) */ @Override public void setProperty(QName propertyName, Object... values) { setPropertyInternal(this.document, propertyName, values); } /** * Sets values for a property in a certain ODocument * * @param propertyName the property name * @param values the values for the property * @param document the document which should contain the data */ protected void setPropertyInternal(ODocument document, QName propertyName, Object... values) { String pName = encodeProperty(propertyName); if (values == null || values.length == 0) { document.removeField(pName); return; } boolean collection = isCollectionProperty(propertyName); if (!collection) { if (values.length > 1) { // TODO log type and property log.warn("Attempt to set multiple values on a property that supports only one, using only the first value"); } document.field(pName, convertInstance(values[0])); } else { List<Object> valueList = new ArrayList<Object>(); for (Object value : values) { valueList.add(convertInstance(value)); } document.field(pName, valueList, getCollectionType(propertyName)); } } /** * Encode a qualified property name to a string * * @param propertyName the qualified property name * @return the name encoded as a single string */ protected String encodeProperty(QName propertyName) { // encode name & map namespace return ONamespaceMap.encode(propertyName); } /** * Decode an encoded property name to a qualified name * * @param encodedProperty the encoded property name * @return the qualified property name */ protected QName decodeProperty(String encodedProperty) { try { // decode name & unmap namespace return ONamespaceMap.decode(encodedProperty); } catch (Throwable e) { throw new RuntimeException("Could not encode property name", e); } } /** * @see Instance#getProperty(QName) */ @Override public Object[] getProperty(QName propertyName) { return getProperty(propertyName, this.document); } /** * Gets a property value from a given {@link ODocument} * * @param propertyName the property name * @param document the {link ODocument} which contains the property * @return an Array of Objects containing the needed property */ protected Object[] getProperty(QName propertyName, ODocument document) { associatedDbWithThread(); String pName = encodeProperty(propertyName); Object value = document.field(pName); if (value == null) { return null; } // cannot check for Iterable as ODocument is also an Iterable else if (value instanceof Collection<?> || value.getClass().isArray()) { List<Object> valueList = new ArrayList<Object>(); for (Object val : (Iterable<?>) value) { valueList.add(convertDocument(val, propertyName)); } return valueList.toArray(); } else { return new Object[] { convertDocument(value, propertyName) }; } } /** * Associate the database with the current thread (if set on the group) */ protected void associatedDbWithThread() { if (db != null) { ODatabaseRecordThreadLocal.INSTANCE.set(db); } } /** * Converts {@link ODocument}s to {@link Instance}s but leaves other objects * untouched. * * @param value the object to convert * @param propertyName the name of the property the value is associated with * @return the converted object */ protected Object convertDocument(Object value, QName propertyName) { return OSerializationHelper.convertFromDB(value, this, propertyName); } /** * @see Group#getPropertyNames() */ @Override public Iterable<QName> getPropertyNames() { return getPropertyNames(this.document); } /** * Returns the index keys of a certain ODocument * * @param document the keys are retrieved from * @return an Iterable with the keys as QNames */ protected Iterable<QName> getPropertyNames(ODocument document) { associatedDbWithThread(); Set<String> fields = new HashSet<String>(Arrays.asList(document.fieldNames())); // remove value field fields.removeAll(getSpecialFieldNames()); Set<QName> qFields = new HashSet<QName>(); for (String field : fields) { qFields.add(decodeProperty(field)); } return qFields; } /** * Get the special field names, e.g. for metadata. * * @return the collection of special field names. */ protected Collection<String> getSpecialFieldNames() { return SPECIAL_FIELDS; } /** * @see Group#getDefinition() */ @Override public DefinitionGroup getDefinition() { return definition; } /** * Get the associated database. * * @return the associated database record */ public ODatabaseRecord getDb() { return db; } }