/* * Copyright (c) 2013-2017 Cinchapi Inc. * * 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 com.cinchapi.concourse.server.storage.db; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import com.cinchapi.concourse.annotate.DoNotInvoke; import com.cinchapi.concourse.annotate.PackagePrivate; import com.cinchapi.concourse.server.model.PrimaryKey; import com.cinchapi.concourse.server.model.Text; import com.cinchapi.concourse.server.model.Value; import com.cinchapi.concourse.server.storage.Action; import com.cinchapi.concourse.server.storage.Versioned; import com.cinchapi.concourse.time.Time; import com.google.common.collect.Maps; import com.google.common.collect.Sets; /** * A logical grouping of data for a single entity. * <p> * This is the primary view of stored data within Concourse, similar to a Row in * a traditional database. PrimaryRecords are designed to efficiently handle * direct/non-query reads. * </p> * * @author Jeff Nelson */ @ThreadSafe @PackagePrivate final class PrimaryRecord extends BrowsableRecord<PrimaryKey, Text, Value> { /** * DO NOT INVOKE. Use {@link Record#createPrimaryRecord(PrimaryKey)} or * {@link Record#createPrimaryRecordPartial(PrimaryKey, Text)} instead. * * @param locator * @param key */ @PackagePrivate @DoNotInvoke protected PrimaryRecord(PrimaryKey locator, @Nullable Text key) { super(locator, key); } /** * Return a log of revision to the entire Record. * * @return the revision log */ public Map<Long, String> audit() { read.lock(); try { Map<Long, String> audit = Maps.newTreeMap(); for (Entry<Text, List<CompactRevision<Value>>> entry : history .entrySet()) { String key = entry.getKey().toString(); for (CompactRevision<Value> revision : entry.getValue()) { audit.put(revision.getVersion(), revision.toString(locator, key)); } } return audit; } finally { read.unlock(); } } /** * Return a log of revisions to the field mapped from {@code key}. * * @param key * @return the revision log */ public Map<Long, String> audit(Text key) { read.lock(); try { Map<Long, String> audit = Maps.newLinkedHashMap(); List<CompactRevision<Value>> revisions = history.get(key); /* Authorized */ if(revisions != null) { Iterator<CompactRevision<Value>> it = revisions.iterator(); while (it.hasNext()) { CompactRevision<Value> revision = it.next(); audit.put(revision.getVersion(), revision.toString(locator, key)); } } return audit; } finally { read.unlock(); } } /** * Return a time series of values that holds the data stored for {@code key} * after each modification. * * @param key the field name * @param start the start timestamp (inclusive) * @param end the end timestamp (exclusive) * @return the time series of values held in the {@code key} field between * {@code start} and {@code end} */ public Map<Long, Set<Value>> chronologize(Text key, long start, long end) { read.lock(); try { Map<Long, Set<Value>> context = Maps.newLinkedHashMap(); List<CompactRevision<Value>> revisions = history.get(key); Set<Value> snapshot = Sets.newLinkedHashSet(); if(revisions != null) { Iterator<CompactRevision<Value>> it = revisions.iterator(); while (it.hasNext()) { CompactRevision<Value> revision = it.next(); long timestamp = revision.getVersion(); if(timestamp >= end) { break; } else { Action action = revision.getType(); snapshot = Sets.newLinkedHashSet(snapshot); Value value = revision.getValue(); if(action == Action.ADD) { snapshot.add(value); } else if(action == Action.REMOVE) { snapshot.remove(value); } if(timestamp >= start && !snapshot.isEmpty()) { context.put(timestamp, snapshot); } } } } if(snapshot.isEmpty()) { // CON-474: If the last snapshot is empty, add it here so that // the Buffer has the proper context context.put(Time.NONE, snapshot); } return context; } finally { read.unlock(); } } /** * Return the Set of values <em>currently</em> contained in the field mapped * from {@code key}. * * @param key * @return the Set of contained values */ public Set<Value> fetch(Text key) { return fetch(key, false, Versioned.NO_VERSION); } /** * Return the Set of values contained in the field mapped from {@code key} * at {@code timestamp}. * * @param key * @param timestamp * @return the Set of contained values */ public Set<Value> fetch(Text key, long timestamp) { return fetch(key, true, timestamp); } /** * Return {@code true} if the Record <em>currently</em> contains data. * * @return {@code true} if {@link #describe()} is not an empty Set */ public boolean ping() { return !describe().isEmpty(); } /** * Return {@code true} if {@code value} <em>currently</em> exists in the * field mapped from {@code key}. * * @param key * @param value * @return {@code true} if {@code key} as {@code value} is a valid mapping */ public boolean verify(Text key, Value value) { return verify(key, value, false, Versioned.NO_VERSION); } /** * Return {@code true} if {@code value} existed in the field mapped from * {@code key} at {@code timestamp} * * @param key * @param value * @param timestamp * @return {@code true} if {@code key} as {@code value} is a valid mapping */ public boolean verify(Text key, Value value, long timestamp) { return verify(key, value, true, timestamp); } @Override protected Map<Text, Set<Value>> mapType() { return Maps.newHashMap(); } /** * Return an unmodifiable view of the Set of values <em>currently</em> * contained in the field mapped from {@code key} or contained at * {@code timestamp} if {@code historical} is {@code true}. * * @param key * @param historical - if {@code true}, read from the history, otherwise * read from the present state * @param timestamp - this value is ignored if {@code historical} is set to * false, otherwise this value is the historical timestamp at * which to read * @return the Set of contained values */ private Set<Value> fetch(Text key, boolean historical, long timestamp) { // NOTE: locking happens in super.get() methods return historical ? get(key, timestamp) : get(key); } /** * Return {@code true} if {@code value} <em>currently</em> exists in the * field mapped from {@code key} or existed in that field at * {@code timestamp} if {@code historical} is {@code true}. * * @param key * @param value * @param historical - if {@code true}, read from the history, otherwise * read from the present state * @param timestamp - this value is ignored if {@code historical} is set to * false, otherwise this value is the historical timestamp at * which to read * @return {@code true} if {@code key} as {@code value} is a valid mapping */ private boolean verify(Text key, Value value, boolean historical, long timestamp) { // NOTE: locking happens in super.get() methods return historical ? get(key, timestamp).contains(value) : get(key) .contains(value); } }