/* * Copyright 2013 NGDATA nv * * 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.fake; import java.io.IOException; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.lilyproject.repository.api.*; import org.lilyproject.repository.impl.FieldTypeBuilderImpl; import org.lilyproject.repository.impl.FieldTypeEntryImpl; import org.lilyproject.repository.impl.FieldTypeImpl; import org.lilyproject.repository.impl.FieldTypesImpl; import org.lilyproject.repository.impl.RecordTypeBuilderImpl; import org.lilyproject.repository.impl.RecordTypeImpl; import org.lilyproject.repository.impl.id.SchemaIdImpl; 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.repo.VersionTag; /** * Dummy type manager that keeps field & record types in memory. No support for caching or snapshots since we don't * really * need this during tests. */ public class FakeTypeManager implements TypeManager { Map<SchemaId, FieldType> fieldTypes = new HashMap<SchemaId, FieldType>(); Map<SchemaId, RecordType> recordTypes = new HashMap<SchemaId, RecordType>(); Map<QName, FieldType> fieldTypesByName = new HashMap<QName, FieldType>(); Map<QName, RecordType> recordTypeByName = new HashMap<QName, RecordType>(); Map<String, ValueTypeFactory> valueTypeFactories = new HashMap<String, ValueTypeFactory>(); private IdGenerator idGenerator; public FakeTypeManager(IdGenerator idGenerator) { this.registerDefaultValueTypes(); this.idGenerator = idGenerator; try { FieldType fieldType = newFieldType(getValueType("LONG"), VersionTag.LAST, Scope.NON_VERSIONED); createFieldType(fieldType); } catch (FieldTypeExistsException e) { // ok } catch (ConcurrentUpdateTypeException e) { // ok, another lily-server is starting up and doing the same thing } catch (RepositoryException e) { // not going to happen here new RuntimeException(e); } catch (InterruptedException e) { // not going to happen here new RuntimeException(e); } } @Override public RecordType newRecordType(QName name) throws TypeException { return new RecordTypeImpl(null, name); } @Override public RecordType newRecordType(SchemaId recordTypeId, QName name) throws TypeException { return new RecordTypeImpl(recordTypeId, name); } @Override public RecordType getRecordTypeById(SchemaId id, Long version) throws RepositoryException, InterruptedException { RecordType type = recordTypes.get(id); if (type == null) { throw new RecordTypeNotFoundException(id, version); } return type; } @Override public RecordType getRecordTypeByName(QName name, Long version) throws RepositoryException, InterruptedException { if (name == null) { return null; } RecordType type = recordTypeByName.get(name); if (type == null) { throw new RecordTypeNotFoundException(name, version); } return type; } @Override public Collection<RecordType> getRecordTypes() throws RepositoryException, InterruptedException { return Lists.newArrayList(recordTypes.values()); } @Override public FieldTypeEntry newFieldTypeEntry(SchemaId fieldTypeId, boolean mandatory) { return new FieldTypeEntryImpl(fieldTypeId, mandatory); } @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 FieldType newFieldType(SchemaId id, ValueType valueType, QName name, Scope scope) { return new FieldTypeImpl(id, valueType, name, scope); } @Override public RecordType createRecordType(RecordType recordType) throws RepositoryException, InterruptedException { return createOrUpdateRecordType(recordType); } @Override public RecordType updateRecordType(RecordType recordType) throws RepositoryException, InterruptedException { return createOrUpdateRecordType(recordType); } @Override public RecordType createOrUpdateRecordType(RecordType recordType) throws RepositoryException, InterruptedException { if (recordType.getId() == null) { recordType.setId(new SchemaIdImpl(UUID.randomUUID())); } Long version = recordType.getVersion(); if (version == null) { version = new Long(0l); } recordType.setVersion(version + 1l); recordTypeByName.put(recordType.getName(), recordType); recordTypes.put(recordType.getId(), recordType); return recordType; } @Override public FieldType createFieldType(FieldType fieldType) throws RepositoryException, InterruptedException { return createOrUpdateFieldType(fieldType); } @Override public FieldType createFieldType(ValueType valueType, QName name, Scope scope) throws RepositoryException, InterruptedException { FieldType fieldType = newFieldType(valueType, name, scope); return createOrUpdateFieldType(fieldType); } @Override public FieldType createFieldType(String valueType, QName name, Scope scope) throws RepositoryException, InterruptedException { FieldType fieldType = newFieldType(valueType, name, scope); return createOrUpdateFieldType(fieldType); } @Override public FieldType updateFieldType(FieldType fieldType) throws RepositoryException, InterruptedException { return createOrUpdateFieldType(fieldType); } @Override public FieldType createOrUpdateFieldType(FieldType fieldType) throws RepositoryException, InterruptedException { if (fieldType.getId() == null) { fieldType.setId(new SchemaIdImpl(UUID.randomUUID())); } fieldTypesByName.put(fieldType.getName(), fieldType); fieldTypes.put(fieldType.getId(), fieldType); return fieldType; } @Override public Pair<List<FieldType>, List<RecordType>> getTypesWithoutCache() throws RepositoryException, InterruptedException { return new Pair<List<FieldType>, List<RecordType>>(getFieldTypesWithoutCache(), getRecordTypesWithoutCache()); } @Override public FieldType getFieldTypeById(SchemaId id) throws RepositoryException, InterruptedException { FieldType type = fieldTypes.get(id); if (type == null) { throw new FieldTypeNotFoundException(id); } return type; } @Override public FieldType getFieldTypeByName(QName name) throws RepositoryException, InterruptedException { FieldType type = fieldTypesByName.get(name); if (type == null) { throw new FieldTypeNotFoundException(name); } return type; } @Override public Collection<FieldTypeEntry> getFieldTypesForRecordType(RecordType recordType, boolean includeSupertypes) throws 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 (Map.Entry<SchemaId, Long> entry : recordType.getSupertypes().entrySet()) { collectRecordSupertypes(Pair.create(entry.getKey(), entry.getValue()), recordSuperTypes); } } @Override public Collection<FieldType> getFieldTypes() throws RepositoryException, InterruptedException { return Lists.newArrayList(fieldTypes.values()); } // // 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()); } @Override public RecordTypeBuilder recordTypeBuilder() throws TypeException { return new RecordTypeBuilderImpl(this); } @Override public FieldTypeBuilder fieldTypeBuilder() throws TypeException { return new FieldTypeBuilderImpl(this); } @Override public org.lilyproject.repository.api.FieldTypes getFieldTypesSnapshot() throws InterruptedException { return new FieldTypesImpl(); } @Override public List<FieldType> getFieldTypesWithoutCache() throws RepositoryException, InterruptedException { return Lists.newArrayList(getFieldTypes()); } @Override public List<RecordType> getRecordTypesWithoutCache() throws RepositoryException, InterruptedException { return Lists.newArrayList(getRecordTypes()); } @Override public TypeBucket getTypeBucketWithoutCache(String bucketId) throws RepositoryException, InterruptedException { throw new UnsupportedOperationException(); } @Override public void enableSchemaCacheRefresh() throws RepositoryException, InterruptedException { throw new UnsupportedOperationException(); } @Override public void disableSchemaCacheRefresh() throws RepositoryException, InterruptedException { throw new UnsupportedOperationException(); } @Override public void triggerSchemaCacheRefresh() throws RepositoryException, InterruptedException { throw new UnsupportedOperationException(); } @Override public boolean isSchemaCacheRefreshEnabled() throws RepositoryException, InterruptedException { return false; } @Override public void close() throws IOException { } @Override public Set<SchemaId> findSubtypes(SchemaId schemaId) throws InterruptedException, RepositoryException { return findSubTypes(schemaId, true); } @Override public Set<SchemaId> findDirectSubtypes(SchemaId schemaId) throws InterruptedException, RepositoryException { return findSubTypes(schemaId, 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, RepositoryException { collectSubTypes(recordTypeId, result, new ArrayDeque<SchemaId>(), recursive); } private void collectSubTypes(SchemaId recordTypeId, Set<SchemaId> result, Deque<SchemaId> parents, boolean recursive) throws InterruptedException, RepositoryException { // 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 = getRecordTypeById(recordTypeId, null).getSupertypes().keySet(); for (SchemaId subtype : subtypes) { if (!parents.contains(subtype)) { result.add(subtype); if (recursive) { collectSubTypes(subtype, result, parents, recursive); } } else { // Loop detected in type hierarchy throw new RepositoryException( "Error while refreshing subtypes of record type " + recordTypeId.toString()); } } parents.pop(); } 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 Set<QName> findSubtypes(QName qName) throws InterruptedException, RepositoryException { return findSubTypes(qName, true); } @Override public Set<QName> findDirectSubtypes(QName qName) throws InterruptedException, RepositoryException { return findSubTypes(qName, false); } @Override public RecordType updateRecordType(RecordType recordType, boolean b) throws RepositoryException, InterruptedException { return updateRecordType(recordType, b, new ArrayDeque<SchemaId>()); } public RecordType createOrUpdateRecordType(RecordType recordType, boolean refreshSubtypes) throws RepositoryException, InterruptedException { if (recordType.getId() != null) { return updateRecordType(recordType, refreshSubtypes); } else { if (recordType.getName() == null) { throw new IllegalArgumentException("No id or name specified in supplied record type."); } boolean exists = this.recordTypeByName.containsKey(recordType.getName()); if (exists) { try { return updateRecordType(recordType, refreshSubtypes); } catch (RecordTypeNotFoundException e) { // record type was renamed in the meantime, retry exists = false; } } else { try { return createRecordType(recordType); } catch (RecordTypeExistsException e) { // record type was created in the meantime, retry exists = true; } } } throw new TypeException("Record type create-or-update failed"); } private RecordType updateRecordType(RecordType recordType, boolean refreshSubtypes, Deque<SchemaId> parents) throws RepositoryException, InterruptedException { // First update the record type RecordType updatedRecordType = updateRecordType(recordType); if (!refreshSubtypes) { return updatedRecordType; } parents.push(updatedRecordType.getId()); try { Set<SchemaId> subtypes = findDirectSubtypes(updatedRecordType.getId()); for (SchemaId subtype : subtypes) { if (!parents.contains(subtype)) { RecordType subRecordType = getRecordTypeById(subtype, null); for (Map.Entry<SchemaId, Long> supertype : subRecordType.getSupertypes().entrySet()) { if (supertype.getKey().equals(updatedRecordType.getId())) { if (!supertype.getValue().equals(updatedRecordType.getVersion())) { subRecordType.addSupertype(updatedRecordType.getId(), updatedRecordType.getVersion()); // Store the change, and recursively adjust the pointers in this record type's subtypes as well updateRecordType(subRecordType, true, parents); } break; } } } else { // Loop detected in type hierarchy, log a warning about this throw new RepositoryException("Loop in the record hierarchy"); } } } catch (RepositoryException e) { throw new RepositoryException("Error while refreshing subtypes of record type " + recordType.getName(), e); } parents.pop(); return updatedRecordType; } }