org.cinchapi.concourse.server.storage.temp.Limbo.java Source code

Java tutorial

Introduction

Here is the source code for org.cinchapi.concourse.server.storage.temp.Limbo.java

Source

/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2013-2014 Jeff Nelson, Cinchapi Software Collective
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.cinchapi.concourse.server.storage.temp;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.locks.ReentrantLock;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import org.cinchapi.concourse.util.TStrings;
import org.cinchapi.concourse.server.model.TObjectSorter;
import org.cinchapi.concourse.server.model.Text;
import org.cinchapi.concourse.server.model.Value;
import org.cinchapi.concourse.server.storage.Action;
import org.cinchapi.concourse.server.storage.BaseStore;
import org.cinchapi.concourse.server.storage.PermanentStore;
import org.cinchapi.concourse.server.storage.VersionGetter;
import org.cinchapi.concourse.server.storage.Versioned;
import org.cinchapi.concourse.server.storage.db.Database;
import org.cinchapi.concourse.thrift.Operator;
import org.cinchapi.concourse.thrift.TObject;
import org.cinchapi.concourse.thrift.Type;
import org.cinchapi.concourse.time.Time;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import static com.google.common.collect.Maps.newLinkedHashMap;

/**
 * {@link Limbo} is a lightweight in-memory proxy store that
 * is a suitable cache or fast, albeit temporary, store for data that will
 * eventually be persisted to a {@link PermanentStore}.
 * <p>
 * The store is designed to write data very quickly <strong>
 * <em>at the expense of much slower read time.</em></strong> {@code Limbo} does
 * not index<sup>1</sup> any of the data it stores, so reads are not as
 * efficient as they would normally be in the {@link Database}.
 * </p>
 * <p>
 * This class provides naive read implementations for the methods specified in
 * the {@link WritableStore} interface, but the subclass is free to override
 * those methods to provide smarter implementations of introduce concurrency
 * controls.
 * </p>
 * <sup>1</sup> - All reads are O(n) because {@code Limbo} uses an
 * {@link #iterator()} to traverse the {@link Write} objects that it stores.
 * 
 * @author jnelson
 */
@NotThreadSafe
public abstract class Limbo extends BaseStore implements Iterable<Write>, VersionGetter {

    /**
     * Return {@code true} if {@code input} matches {@code operator} in relation
     * to {@code values}.
     * 
     * @param input
     * @param operator
     * @param values
     * @return {@code true} if {@code input} matches
     */
    private static boolean matches(Value input, Operator operator, TObject... values) {
        Value v1 = Value.wrap(values[0]);
        switch (operator) {
        case EQUALS:
            return v1.equals(input);
        case NOT_EQUALS:
            return !v1.equals(input);
        case GREATER_THAN:
            return v1.compareTo(input) < 0;
        case GREATER_THAN_OR_EQUALS:
            return v1.compareTo(input) <= 0;
        case LESS_THAN:
            return v1.compareTo(input) > 0;
        case LESS_THAN_OR_EQUALS:
            return v1.compareTo(input) >= 0;
        case BETWEEN:
            Preconditions.checkArgument(values.length > 1);
            Value v2 = Value.wrap(values[1]);
            return v1.compareTo(input) <= 0 && v2.compareTo(input) > 0;
        case REGEX:
            return input.getObject().toString().matches(v1.getObject().toString());
        case NOT_REGEX:
            return !input.getObject().toString().matches(v1.getObject().toString());
        default:
            throw new UnsupportedOperationException();
        }
    }

    /**
     * A Predicate that is used to filter out empty sets.
     */
    private static final Predicate<Set<? extends Object>> emptySetFilter = new Predicate<Set<? extends Object>>() {

        @Override
        public boolean apply(@Nullable Set<? extends Object> input) {
            return !input.isEmpty();
        }

    };

    /**
     * The writeLock ensures that only a single writer can modify the state of
     * the store, without affecting any readers. The subclass should, at a
     * minimum, use this lock in the {@link #insert(Write)} method.
     */
    protected final ReentrantLock writeLock = new ReentrantLock();

    @Override
    public Map<Long, String> audit(long record) {
        Map<Long, String> audit = Maps.newTreeMap();
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getRecord().longValue() == record) {
                audit.put(write.getVersion(), write.toString());
            }
        }
        return audit;

    }

    @Override
    public Map<Long, String> audit(String key, long record) {
        Map<Long, String> audit = Maps.newTreeMap();
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getKey().toString().equals(key) && write.getRecord().longValue() == record) {
                audit.put(write.getVersion(), write.toString());
            }
        }
        return audit;

    }

    @Override
    public Map<String, Set<TObject>> browse(long record) {
        return browse(record, Time.now());
    }

    @Override
    public Map<String, Set<TObject>> browse(long record, long timestamp) {
        Map<String, Set<TObject>> context = Maps.newTreeMap(new Comparator<String>() {

            @Override
            public int compare(String s1, String s2) {
                return s1.compareToIgnoreCase(s2);
            }

        });
        return browse(record, timestamp, context);
    }

    /**
     * Calculate the browsable view of {@code record} at {@code timestamp} using
     * prior {@code context} as if it were also a part of the Buffer.
     * 
     * @param key
     * @param timestamp
     * @param context
     * @return a possibly empty Map of data
     */
    public Map<String, Set<TObject>> browse(long record, long timestamp, Map<String, Set<TObject>> context) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getRecord().longValue() == record && write.getVersion() <= timestamp) {
                Set<TObject> values;
                values = context.get(write.getKey().toString());
                if (values == null) {
                    values = Sets.newHashSet();
                    context.put(write.getKey().toString(), values);
                }
                if (write.getType() == Action.ADD) {
                    values.add(write.getValue().getTObject());
                } else {
                    values.remove(write.getValue().getTObject());
                }
            } else if (write.getVersion() > timestamp) {
                break;
            } else {
                continue;
            }
        }
        return Maps.newTreeMap((SortedMap<String, Set<TObject>>) Maps.filterValues(context, emptySetFilter));
    }

    @Override
    public Map<TObject, Set<Long>> browse(String key) {
        return browse(key, Time.now());
    }

    @Override
    public Map<TObject, Set<Long>> browse(String key, long timestamp) {
        Map<TObject, Set<Long>> context = Maps.newTreeMap(TObjectSorter.INSTANCE);
        return browse(key, timestamp, context);
    }

    /**
     * Calculate the browsable view of {@code key} at {@code timestamp} using
     * prior {@code context} as if it were also a part of the Buffer.
     * 
     * @param key
     * @param timestamp
     * @param context
     * @return a possibly empty Map of data
     */
    public Map<TObject, Set<Long>> browse(String key, long timestamp, Map<TObject, Set<Long>> context) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getKey().toString().equals(key) && write.getVersion() <= timestamp) {
                Set<Long> records = context.get(write.getValue().getTObject());
                if (records == null) {
                    records = Sets.newLinkedHashSet();
                    context.put(write.getValue().getTObject(), records);
                }
                if (write.getType() == Action.ADD) {
                    records.add(write.getRecord().longValue());
                } else {
                    records.remove(write.getRecord().longValue());
                }
            } else if (write.getVersion() > timestamp) {
                break;
            } else {
                continue;
            }
        }
        return Maps.newTreeMap((SortedMap<TObject, Set<Long>>) Maps.filterValues(context, emptySetFilter));
    }

    /**
     * Calculate the description for {@code record} using prior {@code context}
     * as if it were also a part of the Buffer.
     * 
     * @param record
     * @param timestamp
     * @param context
     * @return a possibly empty Set of keys
     */
    public Set<String> describe(long record, long timestamp, Map<String, Set<TObject>> context) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getRecord().longValue() == record && write.getVersion() <= timestamp) {
                Set<TObject> values;
                values = context.get(write.getKey().toString());
                if (values == null) {
                    values = Sets.newHashSet();
                    context.put(write.getKey().toString(), values);
                }
                if (write.getType() == Action.ADD) {
                    values.add(write.getValue().getTObject());
                } else {
                    values.remove(write.getValue().getTObject());
                }
            } else if (write.getVersion() > timestamp) {
                break;
            } else {
                continue;
            }
        }
        return newLinkedHashMap(Maps.filterValues(context, emptySetFilter)).keySet();
    }

    /**
     * This is an implementation of the {@code findAndBrowse} routine that takes
     * in a prior {@code context}. Find and browse will return a mapping from
     * records that match a criteria (expressed as {@code key} filtered by
     * {@code operator} in relation to one or more {@code values}) to the set of
     * values that cause that record to match the criteria.
     * 
     * @param context
     * @param timestamp
     * @param key
     * @param operator
     * @param values
     * @return the relevant data for the records that satisfy the find query
     */
    public Map<Long, Set<TObject>> explore(Map<Long, Set<TObject>> context, long timestamp, String key,
            Operator operator, TObject... values) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            long record = write.getRecord().longValue();
            if (write.getVersion() <= timestamp) {
                if (write.getKey().toString().equals(key) && matches(write.getValue(), operator, values)) {
                    Set<TObject> v = context.get(record);
                    if (v == null) {
                        v = Sets.newHashSet();
                        context.put(record, v);
                    }
                    if (write.getType() == Action.ADD) {
                        v.add(write.getValue().getTObject());
                    } else {
                        v.remove(write.getValue().getTObject());
                        if (v.isEmpty()) {
                            context.remove(record);
                        }
                    }
                }
            } else {
                break;
            }
        }
        return context;
    }

    @Override
    public Set<TObject> fetch(String key, long record) {
        return fetch(key, record, Time.now());
    }

    @Override
    public Set<TObject> fetch(String key, long record, long timestamp) {
        return fetch(key, record, timestamp, Sets.<TObject>newLinkedHashSet());
    }

    /**
     * Fetch the values mapped from {@code key} in {@code record} at
     * {@code timestamp} using prior {@code context} as if it were also a part
     * of the Buffer.
     * 
     * @param key
     * @param record
     * @param timestamp
     * @param context
     * @return the values
     */
    public Set<TObject> fetch(String key, long record, long timestamp, Set<TObject> context) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getVersion() <= timestamp) {
                if (key.equals(write.getKey().toString()) && record == write.getRecord().longValue()) {
                    if (write.getType() == Action.ADD) {
                        context.add(write.getValue().getTObject());
                    } else {
                        context.remove(write.getValue().getTObject());
                    }
                }
            } else {
                break;
            }
        }
        return context;
    }

    @Override
    public long getVersion(long record) {
        return getVersion(null, record);
    }

    @Override
    public long getVersion(String key) {
        Iterator<Write> it = reverseIterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (write.getKey().equals(Text.wrap(key))) {
                return write.getVersion();
            }
        }
        return Versioned.NO_VERSION;
    }

    @Override
    public long getVersion(String key, long record) {
        key = Strings.nullToEmpty(key);
        Iterator<Write> it = reverseIterator();
        while (it.hasNext()) {
            Write write = it.next();
            if (record == write.getRecord().longValue()
                    && (Strings.isNullOrEmpty(key) || write.getKey().equals(Text.wrap(key)))) {
                return write.getVersion();
            }
        }
        return Versioned.NO_VERSION;
    }

    /**
     * Insert {@code write} into the store <strong>without performing any
     * validity checks</strong>.
     * <p>
     * This method is <em>only</em> safe to call from a context that performs
     * its own validity checks (i.e. a {@link BufferedStore}).
     * 
     * @param write
     * @return {@code true}
     */
    public abstract boolean insert(Write write);

    /**
     * {@inheritDoc}
     * <p>
     * <strong>NOTE:</strong> The subclass <em>may</em> override this method to
     * provide an iterator with granular locking functionality for increased
     * throughput.
     * </p>
     */
    @Override
    public abstract Iterator<Write> iterator();

    /**
     * Return an iterator that traverses the Writes in the store in reverse
     * order.
     * 
     * @return the iterator
     */
    public abstract Iterator<Write> reverseIterator();

    @Override
    public Set<Long> search(String key, String query) {
        Map<Long, Set<Value>> rtv = Maps.newHashMap();
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write write = it.next();
            Value value = write.getValue();
            long record = write.getRecord().longValue();
            if (write.getKey().toString().equals(key) && value.getType() == Type.STRING) {
                /*
                 * NOTE: It is not enough to merely check if the stored text
                 * contains the query because the Database does infix
                 * indexing/searching, which has some subtleties:
                 * 1. Stop words are removed from the both stored indices and
                 * the search query
                 * 2. A query and document are considered to match if the
                 * document contains a sequence of terms where each term or a
                 * substring of the term matches the term in the same relative
                 * position of the query.
                 */
                // CON-10: compare lowercase for case insensitive search
                String stored = TStrings.stripStopWords(((String) (value.getObject())).toLowerCase());
                query = TStrings.stripStopWords(query.toLowerCase());
                if (!Strings.isNullOrEmpty(stored) && !Strings.isNullOrEmpty(query)
                        && TStrings.isInfixSearchMatch(query, stored)) {
                    Set<Value> values = rtv.get(record);
                    if (values == null) {
                        values = Sets.newHashSet();
                        rtv.put(record, values);
                    }
                    if (values.contains(value)) {
                        values.remove(value);
                    } else {
                        values.add(value);
                    }

                }
            }
        }
        // FIXME sort search results based on frequency (see
        // SearchRecord#search())
        return newLinkedHashMap(Maps.filterValues(rtv, emptySetFilter)).keySet();
    }

    /**
     * Transport the content of this store to {@code destination}.
     * 
     * @param destination
     */
    public void transport(PermanentStore destination) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            destination.accept(it.next());
            it.remove();
        }

    }

    @Override
    public boolean verify(String key, TObject value, long record) {
        return verify(key, value, record, Time.now());
    }

    @Override
    public boolean verify(String key, TObject value, long record, long timestamp) {
        return verify(Write.notStorable(key, value, record), timestamp);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * currently exists using {@code exists} as prior context.
     * <p>
     * <strong>This method is called from
     * {@link BufferedStore#verify(String, TObject, long)}.</strong>
     * </p>
     * 
     * @param write
     * @return {@code true} if {@code write} currently appears an odd number of
     *         times
     */
    public boolean verify(Write write, boolean exists) {
        return verify(write, Time.now(), exists);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * exists at {@code timestamp}.
     * <p>
     * <strong>This method is called from
     * {@link BufferedStore#verify(String, TObject, long, long)}.</strong>
     * </p>
     * 
     * @param write
     * @param timestamp
     * @return {@code true} if {@code write} appears an odd number of times at
     *         {@code timestamp}
     */
    public boolean verify(Write write, long timestamp) {
        return verify(write, timestamp, false);
    }

    /**
     * Return {@code true} if {@code write} represents a data mapping that
     * exists at {@code timestamp}, using {@code exists} as prior context.
     * <p>
     * <strong>NOTE: ALL OTHER VERIFY METHODS DEFER TO THIS ONE.</strong>
     * </p>
     * 
     * @param write
     * @param timestamp
     * @param exists
     * @return {@code true} if {@code write} appears an odd number of times at
     *         {@code timestamp}
     */
    public boolean verify(Write write, long timestamp, boolean exists) {
        Iterator<Write> it = iterator();
        while (it.hasNext()) {
            Write stored = it.next();
            if (stored.getVersion() <= timestamp) {
                if (stored.equals(write)) {
                    exists ^= true; // toggle boolean
                }
            } else {
                break;
            }
        }
        return exists;
    }

    /**
     * Wait (block) until the Buffer has enough data to complete a transport.
     * This method should be called from the external service to avoid busy
     * waiting if continuously transporting data in the background.
     */
    public void waitUntilTransportable() {
        return; // do nothing because Limbo is assumed to always be
                // transportable. But the Buffer will override this method with
                // the appropriate conditions.
    }

    @Override
    protected Map<Long, Set<TObject>> doExplore(long timestamp, String key, Operator operator, TObject... values) {
        return explore(Maps.<Long, Set<TObject>>newLinkedHashMap(), timestamp, key, operator, values);
    }

    @Override
    protected Map<Long, Set<TObject>> doExplore(String key, Operator operator, TObject... values) {
        return explore(Time.now(), key, operator, values);
    }

}