/* * 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.temp; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import com.cinchapi.common.base.TernaryTruth; import com.cinchapi.concourse.server.model.TObjectSorter; 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.BaseStore; import com.cinchapi.concourse.server.storage.Inventory; import com.cinchapi.concourse.server.storage.PermanentStore; import com.cinchapi.concourse.server.storage.db.Database; import com.cinchapi.concourse.thrift.Operator; import com.cinchapi.concourse.thrift.TObject; import com.cinchapi.concourse.time.Time; import com.cinchapi.concourse.util.MultimapViews; import com.cinchapi.concourse.util.TMaps; import com.cinchapi.concourse.util.TStrings; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; 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 Jeff Nelson */ @NotThreadSafe public abstract class Limbo extends BaseStore implements Iterable<Write> { /** * 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 */ protected static boolean matches(Value input, Operator operator, TObject... values) { Value v1 = Value.wrap(values[0]); switch (operator) { case EQUALS: return v1.compareTo(input) == 0; case NOT_EQUALS: return v1.compareTo(input) != 0; 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. */ protected static final Predicate<Set<? extends Object>> emptySetFilter = new Predicate<Set<? extends Object>>() { @Override public boolean apply(@Nullable Set<? extends Object> input) { return !input.isEmpty(); } }; @Override public Map<Long, String> audit(long record) { Map<Long, String> audit = Maps.newTreeMap(); for (Iterator<Write> it = iterator(); 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(); for (Iterator<Write> it = iterator(); 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<TObject, Set<Long>> browse(String key) { return browse(key, Time.NONE); } @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) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); 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)); } @Override public Map<Long, Set<TObject>> chronologize(String key, long record, long start, long end) { Map<Long, Set<TObject>> context = Maps.newLinkedHashMap(); return chronologize(key, record, start, end, context); } /** * Return a time series that contains the values stored for {@code key} in * {@code record} at each modification timestamp between {@code start} * (inclusive) and {@code end} (exclusive). * * @param key the field name * @param record the record id * @param start the start timestamp (inclusive) * @param end the end timestamp (exclusive) * @param context the prior context * @return a {@link Map mapping} from modification timestamp to a non-empty * {@link Set} of values that were contained at that timestamp */ public Map<Long, Set<TObject>> chronologize(String key, long record, long start, long end, Map<Long, Set<TObject>> context) { Set<TObject> snapshot = Iterables.getLast(context.values(), Sets.<TObject> newLinkedHashSet()); if(snapshot.isEmpty() && !context.isEmpty()) { // CON-474: Empty set is placed in the context if it was the last // snapshot know to the database context.remove(Time.NONE); } for (Iterator<Write> it = iterator(); it.hasNext();) { Write write = it.next(); long timestamp = write.getVersion(); if(timestamp >= end) { break; } else { Text writeKey = write.getKey(); long writeRecord = write.getRecord().longValue(); Action action = write.getType(); if(writeKey.toString().equals(key) && writeRecord == record) { snapshot = Sets.newLinkedHashSet(snapshot); Value writeValue = write.getValue(); if(action == Action.ADD) { snapshot.add(writeValue.getTObject()); } else if(action == Action.REMOVE) { snapshot.remove(writeValue.getTObject()); } if(timestamp >= start && !snapshot.isEmpty()) { context.put(timestamp, snapshot); } } } } return context; } @Override public boolean contains(long record) { for (Iterator<Write> it = iterator(); it.hasNext();) { Write write = it.next(); if(write.getRecord().longValue() == record) { return true; } } return false; } @Override public Set<Long> getAllRecords() { Set<Long> records = Sets.newHashSet(); for (Iterator<Write> it = iterator(); it.hasNext();) { Write write = it.next(); records.add(write.getRecord().longValue()); } return records; } /** * 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) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); 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) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); 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)) { if(write.getType() == Action.ADD) { MultimapViews.put(context, record, write.getValue() .getTObject()); } else { MultimapViews.remove(context, record, write .getValue().getTObject()); } } } else { break; } } } return TMaps.asSortedMap(context); } /** * Return the number of milliseconds that this store desires any back to * back transport requests to pause in between. * * @return the pause time */ public int getDesiredTransportSleepTimeInMs() { return 0; } /** * 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 final boolean insert(Write write) { return insert(write, true); } /** * Insert {@code write} into the store <strong>without performing any * validity checks</strong> and specify whether a {@code sync} should occur * or not. By default, syncs are meaningless in {@link Limbo}, but some * implementations may wish to provide guarantees that the write will be * durably stored. * <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 - The write to append * @param sync - a flag that controls whether this instance will make an * attempt to durably persist the data to some backing store. * Simply ignore this flag if the implementation does not support * durability * @return {@code true} */ public abstract boolean insert(Write write, boolean sync); /** * {@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(); @Override public Set<Long> search(String key, String query) { Map<Long, Set<Value>> rtv = Maps.newHashMap(); String[] needle = TStrings.stripStopWordsAndTokenize(query .toLowerCase()); if(needle.length > 0) { for (Iterator<Write> it = getSearchIterator(key); it.hasNext();) { Write write = it.next(); Value value = write.getValue(); long record = write.getRecord().longValue(); if(isPossibleSearchMatch(key, write, value)) { /* * 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 = (String) (value.getObject()); String[] haystack = TStrings .stripStopWordsAndTokenize(stored.toLowerCase()); if(haystack.length > 0 && TStrings.isInfixSearchMatch(needle, haystack)) { Set<Value> values = rtv.get(record); if(values == null) { values = Sets.newHashSet(); rtv.put(record, values); } if(write.getType() == Action.REMOVE) { values.remove(value); } else { values.add(value); } } } } } // FIXME sort search results based on frequency (see // SearchRecord#search()) return newLinkedHashMap(Maps.filterValues(rtv, emptySetFilter)) .keySet(); } @Override public Map<String, Set<TObject>> select(long record) { return select(record, Time.NONE); } @Override public Map<String, Set<TObject>> select(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 select(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>> select(long record, long timestamp, Map<String, Set<TObject>> context) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); 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 Set<TObject> select(String key, long record) { return select(key, record, Time.NONE); } @Override public Set<TObject> select(String key, long record, long timestamp) { return select(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> select(String key, long record, long timestamp, Set<TObject> context) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); 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; } /** * If the implementation supports durable storage, this method guarantees * that all the data contained here-within is durably persisted. Otherwise, * this method is meaningless and returns immediately. */ public void sync() {/* noop */} /** * Transport the content of this store to {@code destination}. * * @param destination */ public final void transport(PermanentStore destination) { transport(destination, true); } /** * Transport the content of this store to {@code destination} with the * directive to {@code sync} or not. A sync guarantees that the transported * data is durably persisted within the {@link PermanentStore}. * * @param destination - the recipient store for the data * @param syncAfterEach - a flag that controls whether a call is always made * to durably persist (i.e. fsync) in the {@code destination} * after each write is transported */ public void transport(PermanentStore destination, boolean syncAfterEach) { for (Iterator<Write> it = iterator(); it.hasNext();) { destination.accept(it.next(), syncAfterEach); it.remove(); } } @Override public boolean verify(String key, TObject value, long record) { return verify(key, value, record, Time.NONE); } @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.NONE, 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) { if(timestamp >= getOldestWriteTimestamp()) { for (Iterator<Write> it = iterator(); it.hasNext();) { Write stored = it.next(); if(stored.getVersion() <= timestamp) { if(stored.equals(write)) { exists ^= true; // toggle boolean } } else { break; } } } return exists; } /** * A specialized implementation to possibly verify the existence of * {@code write} using three-valued logic. This routine allows the caller to * get a potentially definitive answer by only consulting this store instead * of having to gather prior context beforehand. * <p> * This method will respond in one of three ways when verifying the * existence of {@code write}: * <ul> * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write} * appears in this store at least once and the most recent appearance is the * result of an {@link Action#ADD add} operation.</li> * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write} * appears in the Buffer at least once and the most recent appearance is the * result of a {@link Action#REMOVE remove} operation.</li> * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s * {@link Write#getRecord()} appears is in the inventory AND the * {@code write} does not appear in the Buffer.</li> * </ul> * </p> * * @param write the {@link Write} to verify * @return the appropriate {@link TernaryTruth} value that corresponds to * the Buffer's ability to verify the existence of {@code write} */ public final TernaryTruth verifyFast(Write write) { return verifyFast(write, Time.NONE); } /** * A specialized implementation to possibly verify the existence of * {@code write} at {@code timestamp} using three-valued logic. * This routine allows the caller to get a potentially definitive answer by * only consulting the Buffer instead of having to gather prior context * beforehand. * <p> * This method will respond in one of three ways when verifying the * existence of {@code write} at {@code timestamp}: * <ul> * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}'s * {@link Write#getRecord record} is in the {@link #inventory} AND the * {@code write} appears in the Buffer at least once on or before timestamp * and the appearance most recent to {@code timestamp} is the result of an * {@link Action#ADD add} operation.</li> * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}'s * {@link Write#getRecord record} is NOT in the {@link #inventory} OR the * {@code write} appears in the Buffer at least once on or before timestamp * and the appearance most recent to {@code timestamp} is the result of a * {@link Action#REMOVE remove} operation.</li> * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s * {@link Write#getRecord()} does not appear in this store at * {@code timestamp}</li> * </ul> * </p> * * @param write the {@link Write} to verify * @param timestamp the timestamp at which the verification should happen * @return the appropriate {@link TernaryTruth} value that corresponds to * the store's ability to verify the existence of {@code write} at * {@code timestamp} */ public TernaryTruth verifyFast(Write write, long timestamp) { Action action = getLastWriteAction(write, timestamp); if(action == Action.ADD) { return TernaryTruth.TRUE; } else if(action == Action.REMOVE) { return TernaryTruth.FALSE; } else { return TernaryTruth.UNSURE; } } /** * A specialized implementation to possibly verify the existence of * {@code write} using three-valued logic. This routine allows the caller to * get a potentially definitive answer by only consulting this store instead * of having to gather prior context beforehand. * <p> * This method will respond in one of three ways when verifying the * existence of {@code write}: * <ul> * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write} * appears in this store at least once and the most recent appearance is the * result of an {@link Action#ADD add} operation.</li> * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write} * appears in the Buffer at least once and the most recent appearance is the * result of a {@link Action#REMOVE remove} operation.</li> * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s * {@link Write#getRecord()} appears is in the inventory AND the * {@code write} does not appear in the Buffer.</li> * </ul> * </p> * * @param write the {@link Write} to verify * @param inventory an {@link Inventory} instance to possibly speed up the * verify process * @return the appropriate {@link TernaryTruth} value that corresponds to * the Buffer's ability to verify the existence of {@code write} */ public final TernaryTruth verifyFast(Write write, Inventory inventory) { return verifyFast(write, Time.NONE, inventory); } /** * A specialized implementation to possibly verify the existence of * {@code write} at {@code timestamp} using three-valued logic. * This routine allows the caller to get a potentially definitive answer by * only consulting the Buffer instead of having to gather prior context * beforehand. * <p> * This method will respond in one of three ways when verifying the * existence of {@code write} at {@code timestamp}: * <ul> * <li>Definitively {@link TernaryTruth#TRUE true} if the {@code write}'s * {@link Write#getRecord record} is in the {@link #inventory} AND the * {@code write} appears in the Buffer at least once on or before timestamp * and the appearance most recent to {@code timestamp} is the result of an * {@link Action#ADD add} operation.</li> * <li>Definitively {@link TernaryTruth#TRUE false} if the {@code write}'s * {@link Write#getRecord record} is NOT in the {@link #inventory} OR the * {@code write} appears in the Buffer at least once on or before timestamp * and the appearance most recent to {@code timestamp} is the result of a * {@link Action#REMOVE remove} operation.</li> * <li>{@link TernaryTruth#UNSURE} if the {@code write}'s * {@link Write#getRecord()} does not appear in this store at * {@code timestamp}</li> * </ul> * </p> * * @param write the {@link Write} to verify * @param timestamp the timestamp at which the verification should happen * @param inventory an {@link Inventory} instance to possibly speed up the * verify process * @return the appropriate {@link TernaryTruth} value that corresponds to * the store's ability to verify the existence of {@code write} at * {@code timestamp} */ public TernaryTruth verifyFast(Write write, long timestamp, Inventory inventory) { if(inventory.contains(write.getRecord().longValue())) { return verifyFast(write, timestamp); } else { return TernaryTruth.FALSE; } } /** * 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.NONE, key, operator, values); } /** * Return the timestamp for the oldest write available. * * @return {@code timestamp} */ protected abstract long getOldestWriteTimestamp(); /** * Return the {@link Action} associated with the most recent instance of * {@code write} at {@code timestamp} in the the store. For example, if * {@code timestamp} {@code write} was most recently added, then this method * will return {@link Action#ADD}. * * @param write the comparison {@link Write} whose most recent action is of * interest * @param timestamp the latest timestamp to use when searching * @return the most recent write {@link Action action} or {@code null} if * {@code write} was not present in the store at {@code timestamp} */ @Nullable protected Action getLastWriteAction(Write write, long timestamp) { Action action = null; if(timestamp >= getOldestWriteTimestamp()) { Iterator<Write> it = iterator(); while (it.hasNext()) { Write stored = it.next(); if(stored.getVersion() <= timestamp) { if(stored.equals(write)) { action = stored.getType(); } } else { break; } } } return action; } /** * Return the iterator to use in the {@link #search(String, String)} method. * * @param key * @return the appropriate iterator to use for searching */ protected abstract Iterator<Write> getSearchIterator(String key); /** * Allows the subclass to define some criteria for the search logic to * determine if {@code write} with {@code value} is a possible search match * for {@code key}. * <p> * <strong>NOTE:</strong> This method should NOT check to see if * {@code write} is an true search match for {@code value} because that * logic is handled in the {@link #search(String, String)} method. The * purpose of this method is merely to help quickly eliminate writes that * can't possibly be a search match (i.e. because the write has a non-string * value or a different key). * </p> * <p> * <strong>NOTE:</strong> The {@link Buffer} uses this method to optimize * the check since the iterator it returns in * {@link #getSearchIterator(String)} already ensures that {@code write} has * the same key component as {@code key}. * </p> * * @param key * @param write * @param value * @return {@code true} if the write is a possible search match */ protected abstract boolean isPossibleSearchMatch(String key, Write write, Value value); }