/* * Copyright 2012 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.util.repo; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.lilyproject.repository.api.FieldType; import org.lilyproject.repository.api.FieldTypeNotFoundException; import org.lilyproject.repository.api.IdRecord; import org.lilyproject.repository.api.LRepository; import org.lilyproject.repository.api.LTable; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.RecordId; import org.lilyproject.repository.api.RepositoryException; import org.lilyproject.repository.api.SchemaId; import org.lilyproject.repository.api.Scope; import org.lilyproject.repository.api.TypeManager; /** * This is a record with added logic/state for version tag behavior. */ public class VTaggedRecord { /** * The record containing the last version (or none if non-versioned fields only). */ private final IdRecord record; /** * The record object containing only the non-versioned fields. */ private IdRecord nonVersionedRecord; private Map<SchemaId, Long> vtags; private Map<Long, Set<SchemaId>> tagsByVersion; private RecordEvent recordEvent; private RecordEventHelper recordEventHelper; private SchemaId lastVTag; private LTable table; private TypeManager typeManager; public VTaggedRecord(RecordId recordId, LTable table, LRepository repository) throws RepositoryException, InterruptedException { this(recordId, null, table, repository); } /** * Construct based on a recordId. This will do a repository read to get the latest record. */ public VTaggedRecord(RecordId recordId, RecordEventHelper eventHelper, LTable table, LRepository repository) throws RepositoryException, InterruptedException { // Load the last version of the record to get vtag and non-versioned fields information // We will also reuse this record object in case the last version or the non-versioned data is needed, // to avoid extra gets on HBase. this(table.readWithIds(recordId, null, null), eventHelper, table, repository); } /** * Construct based on an existing record to prevent additional repository reads when the record is already * available. The existing IdRecord should be the last (when it was read) and should have been read with all * fields! */ public VTaggedRecord(IdRecord idRecord, RecordEventHelper eventHelper, LTable table, LRepository repository) throws RepositoryException, InterruptedException { this.table = table; this.typeManager = repository.getTypeManager(); this.record = idRecord; this.recordEventHelper = eventHelper; if (eventHelper != null) { this.recordEvent = eventHelper.getEvent(); } } public RecordId getId() { return record.getId(); } /** * Returns the record object of the last version of the record, or the non-versioned record object if the * record has no versions. */ public IdRecord getRecord() { return record; } public IdRecord getNonVersionedRecord() throws RepositoryException, InterruptedException { if (nonVersionedRecord == null) { if (record.getVersion() == null) { // record has no version, so no versioned fields, so no cloning necessary this.nonVersionedRecord = record; } else { IdRecord nonVersionedRecord = record.cloneRecord(); reduceToNonVersioned(nonVersionedRecord, typeManager); this.nonVersionedRecord = nonVersionedRecord; } } return nonVersionedRecord; } public SchemaId getLastVTag() throws RepositoryException, InterruptedException { if (lastVTag == null) { lastVTag = typeManager.getFieldTypeByName(VersionTag.LAST).getId(); } return lastVTag; } /** * The set of vtags defined on the record, including the last vtag. * * <p>Note that version numbers do not necessarily correspond to existing versions, a user might * have defined invalid vtags. */ public Map<SchemaId, Long> getVTags() throws InterruptedException, RepositoryException { if (vtags == null) { vtags = getTagsById(record, typeManager); } return vtags; } private Map<SchemaId, Long> getTagsById(IdRecord record, TypeManager typeManager) throws InterruptedException, RepositoryException { Map<SchemaId, Long> vtags = new HashMap<SchemaId, Long>(); for (Map.Entry<SchemaId, Object> field : record.getFieldsById().entrySet()) { FieldType fieldType; try { fieldType = typeManager.getFieldTypeById(field.getKey()); } catch (FieldTypeNotFoundException e) { // A field whose field type does not exist: skip it continue; } if (VersionTag.isVersionTag(fieldType)) { vtags.put(fieldType.getId(), (Long) field.getValue()); } } vtags.put(getLastVTag(), record.getVersion() == null ? 0 : record.getVersion()); return vtags; } public RecordEvent getRecordEvent() { return recordEvent; } public RecordEventHelper getRecordEventHelper() { return recordEventHelper; } public Set<SchemaId> getVTagsOfModifiedData() throws RepositoryException, InterruptedException { Set<SchemaId> vtagsOfChangedData = null; Map<Scope, Set<FieldType>> updatedFieldsByScope = recordEventHelper.getUpdatedFieldsByScope(); // Make sure these are calculated getVTags(); getVTagsByVersion(); // If non-versioned fields changed: all vtags are affected since each vtag-based view also includes the // non-versioned fields // Note that the updated fields also include deleted fields, as it should. if (!updatedFieldsByScope.get(Scope.NON_VERSIONED).isEmpty()) { vtagsOfChangedData = vtags.keySet(); } else if (!updatedFieldsByScope.get(Scope.VERSIONED).isEmpty() || !updatedFieldsByScope.get(Scope.VERSIONED_MUTABLE).isEmpty()) { if (recordEvent.getVersionCreated() != -1) { vtagsOfChangedData = tagsByVersion.get(recordEvent.getVersionCreated()); } else if (recordEvent.getVersionUpdated() != -1) { vtagsOfChangedData = tagsByVersion.get(recordEvent.getVersionUpdated()); } } return vtagsOfChangedData != null ? vtagsOfChangedData : Collections.<SchemaId>emptySet(); } public Map<Long, Set<SchemaId>> getVTagsByVersion() throws InterruptedException, RepositoryException { if (tagsByVersion == null) { tagsByVersion = idTagsByVersion(getVTags()); } return tagsByVersion; } /** * Inverts a map containing version by tag to a map containing id tags by version. */ private Map<Long, Set<SchemaId>> idTagsByVersion(Map<SchemaId, Long> vtags) { Map<Long, Set<SchemaId>> result = new HashMap<Long, Set<SchemaId>>(); for (Map.Entry<SchemaId, Long> entry : vtags.entrySet()) { Set<SchemaId> tags = result.get(entry.getValue()); if (tags == null) { tags = new HashSet<SchemaId>(); result.put(entry.getValue(), tags); } tags.add(entry.getKey()); } return result; } public IdRecord getIdRecord(SchemaId vtagId) throws InterruptedException, RepositoryException { return getIdRecord(vtagId, null); } public IdRecord getIdRecord(SchemaId vtagId, List<SchemaId> fields) throws InterruptedException, RepositoryException { Long version = getVTags().get(vtagId); if (version == null) { return null; } return getIdRecord(version, fields); } public IdRecord getIdRecord(long version) throws InterruptedException, RepositoryException { return getIdRecord(version, null); } private IdRecord getIdRecord(long version, List<SchemaId> fields) throws InterruptedException, RepositoryException { // TODO in case of the cached copies, we should filter the fields to the requested fields (not used anywhere // at the time of this writing) if (version == 0L) { return getNonVersionedRecord(); } else if (record.getVersion() != null && version == record.getVersion()) { return record; } else { return table.readWithIds(record.getId(), version, fields); } } /** * Removes any versioned information from the supplied record object. * * <p>This method can be removed once we have a repository method that is able to filter this when loading * the record. */ public static void reduceToNonVersioned(IdRecord record, TypeManager typeManager) throws RepositoryException, InterruptedException { if (record.getVersion() == null) { // The record has no versions so there should be no versioned fields in it return; } // Remove all non-versioned fields from the record Map<SchemaId, QName> mapping = record.getFieldIdToNameMapping(); Iterator<Map.Entry<SchemaId, QName>> it = mapping.entrySet().iterator(); while (it.hasNext()) { Map.Entry<SchemaId, QName> entry = it.next(); if (typeManager.getFieldTypeById(entry.getKey()).getScope() != Scope.NON_VERSIONED) { record.delete(entry.getValue(), false); it.remove(); } } // Remove versioned record type info record.setRecordType(Scope.VERSIONED, (QName) null, null); record.setRecordType(Scope.VERSIONED_MUTABLE, (QName) null, null); } }