/* * Copyright 2010 Outerthought bvba * * 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.lilyproject.repository.impl; import com.google.common.collect.Maps; import org.apache.commons.logging.Log; import org.lilyproject.repository.api.FieldType; import org.lilyproject.repository.api.FieldTypeBuilder; import org.lilyproject.repository.api.FieldTypeEntry; import org.lilyproject.repository.api.FieldTypes; import org.lilyproject.repository.api.IdGenerator; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.RecordType; import org.lilyproject.repository.api.RecordTypeBuilder; import org.lilyproject.repository.api.RecordTypeNotFoundException; import org.lilyproject.repository.api.RepositoryException; import org.lilyproject.repository.api.SchemaId; import org.lilyproject.repository.api.Scope; import org.lilyproject.repository.api.TypeException; import org.lilyproject.repository.api.TypeManager; import org.lilyproject.repository.api.ValueType; import org.lilyproject.repository.api.ValueTypeFactory; import org.lilyproject.repository.impl.valuetype.BlobValueType; import org.lilyproject.repository.impl.valuetype.BooleanValueType; import org.lilyproject.repository.impl.valuetype.ByteArrayValueType; import org.lilyproject.repository.impl.valuetype.DateTimeValueType; import org.lilyproject.repository.impl.valuetype.DateValueType; import org.lilyproject.repository.impl.valuetype.DecimalValueType; import org.lilyproject.repository.impl.valuetype.DoubleValueType; import org.lilyproject.repository.impl.valuetype.IntegerValueType; import org.lilyproject.repository.impl.valuetype.LinkValueType; import org.lilyproject.repository.impl.valuetype.ListValueType; import org.lilyproject.repository.impl.valuetype.LongValueType; import org.lilyproject.repository.impl.valuetype.PathValueType; import org.lilyproject.repository.impl.valuetype.RecordValueType; import org.lilyproject.repository.impl.valuetype.StringValueType; import org.lilyproject.repository.impl.valuetype.UriValueType; import org.lilyproject.util.ArgumentValidator; import org.lilyproject.util.Pair; import org.lilyproject.util.zookeeper.ZooKeeperItf; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public abstract class AbstractTypeManager implements TypeManager { protected Log log; protected Map<String, ValueTypeFactory> valueTypeFactories = new HashMap<String, ValueTypeFactory>(); protected IdGenerator idGenerator; protected ZooKeeperItf zooKeeper; protected SchemaCache schemaCache; public AbstractTypeManager(ZooKeeperItf zooKeeper) { this.zooKeeper = zooKeeper; } @Override public FieldTypes getFieldTypesSnapshot() throws InterruptedException { return schemaCache.getFieldTypesSnapshot(); } @Override abstract public List<FieldType> getFieldTypesWithoutCache() throws RepositoryException, InterruptedException; @Override abstract public List<RecordType> getRecordTypesWithoutCache() throws RepositoryException, InterruptedException; protected void updateFieldTypeCache(FieldType fieldType) throws TypeException, InterruptedException { schemaCache.updateFieldType(fieldType); } protected void updateRecordTypeCache(RecordType recordType) throws TypeException, InterruptedException { schemaCache.updateRecordType(recordType); } @Override public Collection<RecordType> getRecordTypes() throws InterruptedException { return schemaCache.getRecordTypes(); } @Override public List<FieldType> getFieldTypes() throws TypeException, InterruptedException { return schemaCache.getFieldTypes(); } protected RecordType getRecordTypeFromCache(QName name, Long version) throws InterruptedException { return schemaCache.getRecordType(name, version); } protected RecordType getRecordTypeFromCache(SchemaId id, Long version) { return schemaCache.getRecordType(id, version); } @Override public RecordType getRecordTypeById(SchemaId id, Long version) throws RecordTypeNotFoundException, TypeException, RepositoryException, InterruptedException { ArgumentValidator.notNull(id, "id"); RecordType recordType = getRecordTypeFromCache(id, version); if (recordType == null) { throw new RecordTypeNotFoundException(id, version); } return recordType.clone(); } @Override public RecordType getRecordTypeByName(QName name, Long version) throws RecordTypeNotFoundException, TypeException, RepositoryException, InterruptedException { ArgumentValidator.notNull(name, "name"); RecordType recordType = getRecordTypeFromCache(name, version); if (recordType == null) { throw new RecordTypeNotFoundException(name, version); } return recordType.clone(); } @Override public Set<QName> findSubtypes(QName recordTypeName) throws InterruptedException, RepositoryException { return findSubTypes(recordTypeName, true); } @Override public Set<QName> findDirectSubtypes(QName recordTypeName) throws InterruptedException, RepositoryException { return findSubTypes(recordTypeName, false); } private Set<QName> findSubTypes(QName recordTypeName, boolean recursive) throws InterruptedException, RepositoryException { ArgumentValidator.notNull(recordTypeName, "recordTypeName"); RecordType recordType = getRecordTypeByName(recordTypeName, null); Set<SchemaId> result = new HashSet<SchemaId>(); collectSubTypes(recordType.getId(), result, recursive); // Translate schema id's to QName's Set<QName> names = new HashSet<QName>(); for (SchemaId id : result) { try { names.add(getRecordTypeById(id, null).getName()); } catch (RecordTypeNotFoundException e) { // skip, this should only occur in border cases, i.e. the schema is being modified while // this method is called } } return names; } @Override public Collection<FieldTypeEntry> getFieldTypesForRecordType(RecordType recordType, boolean includeSupertypes) throws RecordTypeNotFoundException, TypeException, RepositoryException, InterruptedException { if (!includeSupertypes) { return recordType.getFieldTypeEntries(); } else { // Pairs of record type id and version Map<Pair<SchemaId, Long>, RecordType> recordSupertypeMap = Maps.newHashMap(); collectRecordSupertypes(Pair.create(recordType.getId(), recordType.getVersion()), recordSupertypeMap); // We use a map of SchemaId to FieldTypeEntry so that we can let mandatory field type entries // for the same field type override non-mandatory versions Map<SchemaId, FieldTypeEntry> fieldTypeMap = Maps.newHashMap(); for (Pair<SchemaId, Long> recordSuperTypePair : recordSupertypeMap.keySet()) { RecordType superRecordType = recordSupertypeMap.get(recordSuperTypePair); for (FieldTypeEntry fieldTypeEntry : superRecordType.getFieldTypeEntries()) { SchemaId fieldTypeId = fieldTypeEntry.getFieldTypeId(); if (fieldTypeMap.containsKey(fieldTypeId)) { // Only overwrite an existing entry if we have one that is mandatory if (fieldTypeEntry.isMandatory()) { fieldTypeMap.put(fieldTypeId, fieldTypeEntry); } } else { fieldTypeMap.put(fieldTypeId, fieldTypeEntry); } } } return fieldTypeMap.values(); } } private void collectRecordSupertypes(Pair<SchemaId, Long> recordTypeAndVersion, Map<Pair<SchemaId, Long>, RecordType> recordSuperTypes) throws RecordTypeNotFoundException, TypeException, RepositoryException, InterruptedException { if (recordSuperTypes.containsKey(recordTypeAndVersion)) { return; } RecordType recordType = getRecordTypeById(recordTypeAndVersion.getV1(), recordTypeAndVersion.getV2()); recordSuperTypes.put(recordTypeAndVersion, recordType); for (Entry<SchemaId, Long> entry : recordType.getSupertypes().entrySet()) { collectRecordSupertypes(Pair.create(entry.getKey(), entry.getValue()), recordSuperTypes); } } @Override public Set<SchemaId> findSubtypes(SchemaId recordTypeId) throws InterruptedException, RepositoryException { return findSubTypes(recordTypeId, true); } @Override public Set<SchemaId> findDirectSubtypes(SchemaId recordTypeId) throws InterruptedException, RepositoryException { return findSubTypes(recordTypeId, false); } private Set<SchemaId> findSubTypes(SchemaId recordTypeId, boolean recursive) throws InterruptedException, RepositoryException { ArgumentValidator.notNull(recordTypeId, "recordTypeId"); // This is to validate the requested ID exists getRecordTypeById(recordTypeId, null); Set<SchemaId> result = new HashSet<SchemaId>(); collectSubTypes(recordTypeId, result, recursive); return result; } private void collectSubTypes(SchemaId recordTypeId, Set<SchemaId> result, boolean recursive) throws InterruptedException { collectSubTypes(recordTypeId, result, new ArrayDeque<SchemaId>(), recursive); } private void collectSubTypes(SchemaId recordTypeId, Set<SchemaId> result, Deque<SchemaId> parents, boolean recursive) throws InterruptedException { // the parent-stack is to protect against endless loops in the type hierarchy. If a type is a subtype // of itself, it will not be included in the result. Thus if record type A extends (directly or indirectly) // from A, and we search the subtypes of A, then the resulting set will not include A. parents.push(recordTypeId); Set<SchemaId> subtypes = schemaCache.findDirectSubTypes(recordTypeId); for (SchemaId subtype : subtypes) { if (!parents.contains(subtype)) { result.add(subtype); if (recursive) { collectSubTypes(subtype, result, parents, recursive); } } else { // Loop detected in type hierarchy, log a warning about this log.warn(formatSupertypeLoopError(subtype, parents)); } } parents.pop(); } protected String formatSupertypeLoopError(SchemaId subtype, Deque<SchemaId> parents) { // Loop detected in type hierarchy, log a warning about this List<SchemaId> parentsList = new ArrayList<SchemaId>(parents); Collections.reverse(parentsList); // find the place where the current childType occurred (we don't want to log any parents higher // up, doesn't add any value for the user) int pos = parentsList.indexOf(subtype); StringBuilder msg = new StringBuilder(); for (int i = pos; i < parentsList.size(); i++) { if (msg.length() > 0) { msg.append(" <- "); } msg.append(getNameSafe(parentsList.get(i))); } msg.append(" <- "); msg.append(getNameSafe(subtype)); return "Loop in record type inheritance: " + msg; } /** * Tries to look up the name of the record type, but returns the id if it fails. */ private String getNameSafe(SchemaId schemaId) { try { return getRecordTypeById(schemaId, null).getName().toString(); } catch (Exception e) { return schemaId.toString(); } } @Override public FieldType getFieldTypeById(SchemaId id) throws TypeException, InterruptedException { return schemaCache.getFieldType(id); } @Override public FieldType getFieldTypeByName(QName name) throws InterruptedException, TypeException { return schemaCache.getFieldType(name); } // // Object creation methods // @Override public RecordType newRecordType(QName name) { return new RecordTypeImpl(null, name); } @Override public RecordType newRecordType(SchemaId recordTypeId, QName name) { return new RecordTypeImpl(recordTypeId, name); } @Override public FieldType newFieldType(ValueType valueType, QName name, Scope scope) { return newFieldType(null, valueType, name, scope); } @Override public FieldType newFieldType(String valueType, QName name, Scope scope) throws RepositoryException, InterruptedException { return newFieldType(null, getValueType(valueType), name, scope); } @Override public FieldTypeEntry newFieldTypeEntry(SchemaId fieldTypeId, boolean mandatory) { ArgumentValidator.notNull(fieldTypeId, "fieldTypeId"); ArgumentValidator.notNull(mandatory, "mandatory"); return new FieldTypeEntryImpl(fieldTypeId, mandatory); } @Override public FieldType newFieldType(SchemaId id, ValueType valueType, QName name, Scope scope) { return new FieldTypeImpl(id, valueType, name, scope); } @Override public RecordTypeBuilder recordTypeBuilder() throws TypeException { return new RecordTypeBuilderImpl(this); } @Override public FieldTypeBuilder fieldTypeBuilder() throws TypeException { return new FieldTypeBuilderImpl(this); } // // Value types // @Override public void registerValueType(String valueTypeName, ValueTypeFactory valueTypeFactory) { valueTypeFactories.put(valueTypeName, valueTypeFactory); } @Override public ValueType getValueType(String valueTypeSpec) throws RepositoryException, InterruptedException { ValueType valueType; int indexOfParams = valueTypeSpec.indexOf("<"); if (indexOfParams == -1) { ValueTypeFactory valueTypeFactory = valueTypeFactories.get(valueTypeSpec); if (valueTypeFactory == null) { throw new TypeException("Unkown value type: " + valueTypeSpec); } valueType = valueTypeFactory.getValueType(null); } else { if (!valueTypeSpec.endsWith(">")) { throw new IllegalArgumentException("Invalid value type string, no closing angle bracket: '" + valueTypeSpec + "'"); } String arg = valueTypeSpec.substring(indexOfParams + 1, valueTypeSpec.length() - 1); if (arg.length() == 0) { throw new IllegalArgumentException("Invalid value type string, type arg is zero length: '" + valueTypeSpec + "'"); } ValueTypeFactory valueTypeFactory = valueTypeFactories.get(valueTypeSpec.substring(0, indexOfParams)); if (valueTypeFactory == null) { throw new TypeException("Unkown value type: " + valueTypeSpec); } valueType = valueTypeFactory.getValueType(arg); } return valueType; } // TODO get this from some configuration file protected void registerDefaultValueTypes() { // // Important: // // When adding a type below, please update the list of built-in // types in the javadoc of the method TypeManager.getValueType. // // TODO or rather use factories? registerValueType(StringValueType.NAME, StringValueType.factory()); registerValueType(IntegerValueType.NAME, IntegerValueType.factory()); registerValueType(LongValueType.NAME, LongValueType.factory()); registerValueType(DoubleValueType.NAME, DoubleValueType.factory()); registerValueType(DecimalValueType.NAME, DecimalValueType.factory()); registerValueType(BooleanValueType.NAME, BooleanValueType.factory()); registerValueType(DateValueType.NAME, DateValueType.factory()); registerValueType(DateTimeValueType.NAME, DateTimeValueType.factory()); registerValueType(LinkValueType.NAME, LinkValueType.factory(idGenerator, this)); registerValueType(BlobValueType.NAME, BlobValueType.factory()); registerValueType(UriValueType.NAME, UriValueType.factory()); registerValueType(ListValueType.NAME, ListValueType.factory(this)); registerValueType(PathValueType.NAME, PathValueType.factory(this)); registerValueType(RecordValueType.NAME, RecordValueType.factory(this)); registerValueType(ByteArrayValueType.NAME, ByteArrayValueType.factory()); } }