com.cinchapi.concourse.util.Convert.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.concourse.util.Convert.java

Source

/*
 * Copyright (c) 2013-2016 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.util;

import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.concurrent.Immutable;

import com.cinchapi.concourse.Concourse;
import com.cinchapi.concourse.Link;
import com.cinchapi.concourse.Tag;
import com.cinchapi.concourse.annotate.PackagePrivate;
import com.cinchapi.concourse.annotate.UtilityClass;
import com.cinchapi.concourse.thrift.Operator;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.thrift.Type;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Longs;
import com.google.gson.JsonParseException;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

/**
 * A collection of functions to convert objects. The public API defined in
 * {@link Concourse} uses certain objects for convenience that are not
 * recognized by Thrift, so it is necessary to convert back and forth between
 * different representations.
 * 
 * @author Jeff Nelson
 */
@UtilityClass
public final class Convert {

    /**
     * Takes a JSON string representation of an object or an array of JSON
     * objects and returns a list of {@link Multimap multimaps} with the
     * corresponding data. Unlike {@link #jsonToJava(String)}, this method will
     * allow the top level element to be an array in the {code json} string.
     * 
     * @param json
     * @return A list of Java objects
     */
    public static List<Multimap<String, Object>> anyJsonToJava(String json) {
        List<Multimap<String, Object>> result = Lists.newArrayList();
        try (JsonReader reader = new JsonReader(new StringReader(json))) {
            reader.setLenient(true);
            if (reader.peek() == JsonToken.BEGIN_ARRAY) {
                try {
                    reader.beginArray();
                    while (reader.peek() != JsonToken.END_ARRAY) {
                        result.add(jsonToJava(reader));
                    }
                    reader.endArray();
                } catch (IllegalStateException e) {
                    throw new JsonParseException(e.getMessage());
                }
            } else {
                result.add(jsonToJava(reader));
            }
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
        return result;
    }

    /**
     * Return a List that represents the Thrift representation of each of the
     * {@code objects} in the input list.
     * 
     * @param objects a List of java objects
     * @return a List of TObjects
     */
    public static List<TObject> javaListToThrift(List<Object> objects) {
        List<TObject> thrift = Lists.newArrayListWithCapacity(objects.size());
        javaCollectionToThrift(objects, thrift);
        return thrift;
    }

    /**
     * Return a Map that represents the Thrift representation of each of the
     * {@code objects} in the input Map.
     * 
     * @param objects a Map of java objects
     * @return a Map of TObjects
     */
    public static <K> Map<K, TObject> javaMapToThrift(Map<K, Object> objects) {
        Map<K, TObject> thrift = Maps.newLinkedHashMap();
        for (Entry<K, Object> entry : objects.entrySet()) {
            K key = entry.getKey();
            TObject value = javaToThrift(entry.getValue());
            thrift.put(key, value);
        }
        return thrift;
    }

    /**
     * Return a Set that represents the Thrift representation of each of the
     * {@code objects} in the input Set.
     * 
     * @param objects a Set of java objects
     * @return a Set of TObjects
     */
    public static Set<TObject> javaSetToThrift(Set<Object> objects) {
        Set<TObject> thrift = Sets.newLinkedHashSetWithExpectedSize(objects.size());
        javaCollectionToThrift(objects, thrift);
        return thrift;
    }

    /**
     * Return the Thrift Object that represents {@code object}.
     * 
     * @param object
     * @return the TObject
     */
    public static TObject javaToThrift(Object object) {
        ByteBuffer bytes;
        Type type = null;
        if (object instanceof Boolean) {
            bytes = ByteBuffer.allocate(1);
            bytes.put((boolean) object ? (byte) 1 : (byte) 0);
            type = Type.BOOLEAN;
        } else if (object instanceof Double) {
            bytes = ByteBuffer.allocate(8);
            bytes.putDouble((double) object);
            type = Type.DOUBLE;
        } else if (object instanceof Float) {
            bytes = ByteBuffer.allocate(4);
            bytes.putFloat((float) object);
            type = Type.FLOAT;
        } else if (object instanceof Link) {
            bytes = ByteBuffer.allocate(8);
            bytes.putLong(((Link) object).longValue());
            type = Type.LINK;
        } else if (object instanceof Long) {
            bytes = ByteBuffer.allocate(8);
            bytes.putLong((long) object);
            type = Type.LONG;
        } else if (object instanceof Integer) {
            bytes = ByteBuffer.allocate(4);
            bytes.putInt((int) object);
            type = Type.INTEGER;
        } else if (object instanceof Tag) {
            bytes = ByteBuffer.wrap(object.toString().getBytes(StandardCharsets.UTF_8));
            type = Type.TAG;
        } else {
            bytes = ByteBuffer.wrap(object.toString().getBytes(StandardCharsets.UTF_8));
            type = Type.STRING;
        }
        bytes.rewind();
        return new TObject(bytes, type).setJavaFormat(object);
    }

    /**
     * Convert a JSON formatted string to a mapping that associates each key
     * with the Java objects that represent the corresponding values. This
     * method is designed to parse simple JSON structures that associate keys to
     * simple values or arrays without knowing the type of each element ahead of
     * time.
     * <p>
     * This method can properly handle JSON strings that abide by the following
     * rules:
     * <ul>
     * <li>The top level element in the JSON string must be an Object</li>
     * <li>No nested objects (e.g. a key cannot map to an object)</li>
     * <li>No null values</li>
     * </ul>
     * </p>
     * 
     * @param json
     * @return the converted data
     */
    public static Multimap<String, Object> jsonToJava(String json) {
        try (JsonReader reader = new JsonReader(new StringReader(json))) {
            reader.setLenient(true);
            return jsonToJava(reader);
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Serialize the {@link list} of {@link Multimap maps} with data to a JSON
     * string that can be batch inserted into Concourse.
     * 
     * @param list the list of data {@link Multimap maps} to include in the JSON
     *            object. This is meant to map to the return value of
     *            {@link #anyJsonToJava(String)}
     * @return the JSON string representation of the {@code list}
     */
    @SuppressWarnings("unchecked")
    public static String mapsToJson(Collection<Multimap<String, Object>> list) {
        // GH-116: The signature declares that the list should contain Multimap
        // instances, but we check the type of each element in case the data is
        // coming from a JVM dynamic language (i.e. Groovy) that has syntactic
        // sugar for a java.util.Map
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (Object map : list) {
            if (map instanceof Multimap) {
                Multimap<String, Object> map0 = (Multimap<String, Object>) map;
                sb.append(mapToJson(map0));
                sb.append(",");
            } else if (map instanceof Map) {
                Map<String, Object> map0 = (Map<String, Object>) map;
                sb.append(mapToJson(map0));
                sb.append(",");
            } else {
                ((Multimap<String, Object>) map).getClass(); // force
                                                             // ClassCastException
                                                             // to be thrown
            }

        }
        sb.setCharAt(sb.length() - 1, ']');
        return sb.toString();
    }

    /**
     * Serialize the {@code map} of data as a JSON object string that can be
     * inserted into Concourse.
     * 
     * @param map data to include in the JSON object.
     * @return the JSON string representation of the {@code map}
     */
    public static String mapToJson(Map<String, ?> map) {
        return DataServices.gson().toJson(map);
    }

    /**
     * Serialize the {@code map} of a data as a JSON string that can inserted
     * into Concourse.
     * 
     * @param map the data to include in the JSON object. This is meant to map
     *            to the return value of {@link #jsonToJava(String)}
     * @return the JSON string representation of the {@code map}
     */
    public static String mapToJson(Multimap<String, Object> map) {
        return mapToJson(map.asMap());
    }

    /**
     * Convert the {@code operator} to a string representation.
     * 
     * @param operator
     * @return the operator string
     */
    public static String operatorToString(Operator operator) {
        String string = "";
        switch (operator) {
        case EQUALS:
            string = "=";
            break;
        case NOT_EQUALS:
            string = "!=";
            break;
        case GREATER_THAN:
            string = ">";
            break;
        case GREATER_THAN_OR_EQUALS:
            string = ">=";
            break;
        case LESS_THAN:
            string = "<";
            break;
        case LESS_THAN_OR_EQUALS:
            string = "<=";
            break;
        case BETWEEN:
            string = "><";
            break;
        default:
            string = operator.name();
            break;

        }
        return string;
    }

    /**
     * For a scalar object that may be a {@link TObject} or a collection of
     * other objects that may contain {@link TObject TObjects}, convert to the
     * appropriate java representation.
     * 
     * @param tobject the possible TObject or collection of TObjects
     * @return the java representation
     */
    @SuppressWarnings("unchecked")
    public static <T> T possibleThriftToJava(Object tobject) {
        if (tobject instanceof TObject) {
            return (T) thriftToJava((TObject) tobject);
        } else if (tobject instanceof List) {
            return (T) Lists.transform((List<?>) tobject, Conversions.possibleThriftToJava());
        } else if (tobject instanceof Set) {
            return (T) Transformers.transformSetLazily((Set<Object>) tobject, Conversions.possibleThriftToJava());
        } else if (tobject instanceof Map) {
            return (T) Transformers.transformMapEntries((Map<Object, Object>) tobject,
                    Conversions.possibleThriftToJava(), Conversions.possibleThriftToJava());
        } else {
            return (T) tobject;
        }
    }

    /**
     * Analyze {@code value} and convert it to the appropriate Java primitive or
     * Object.
     * <p>
     * <h1>Conversion Rules</h1>
     * <ul>
     * <li><strong>String</strong> - the value is converted to a string if it
     * starts and ends with matching single (') or double ('') quotes.
     * Alternatively, the value is converted to a string if it cannot be
     * converted to another type</li>
     * <li><strong>{@link ResolvableLink}</strong> - the value is converted to a
     * ResolvableLink if it is a properly formatted specification returned from
     * the {@link #stringToResolvableLinkSpecification(String, String)} method
     * (<strong>NOTE: </strong> this is a rare case)</li>
     * <li><strong>{@link Link}</strong> - the value is converted to a Link if
     * it is an int or long that is wrapped by '@' signs (i.e. @1234@)</li>
     * <li><strong>Boolean</strong> - the value is converted to a Boolean if it
     * is equal to 'true', or 'false' regardless of case</li>
     * <li><strong>Double</strong> - the value is converted to a double if and
     * only if it is a decimal number that is immediately followed by a single
     * capital "D" (e.g. 3.14D)</li>
     * <li><strong>Tag</strong> - the value is converted to a Tag if it starts
     * and ends with matching (`) quotes</li>
     * <li><strong>Integer, Long, Float</strong> - the value is converted to a
     * non double number depending upon whether it is a standard integer (e.g.
     * less than {@value java.lang.Integer#MAX_VALUE}), a long, or a floating
     * point decimal</li>
     * </ul>
     * </p>
     * 
     * 
     * @param value
     * @return the converted value
     */
    public static Object stringToJava(String value) {
        if (value.isEmpty()) {
            return value;
        }
        char first = value.charAt(0);
        char last = value.charAt(value.length() - 1);
        Long record;
        if (Strings.isWithinQuotes(value)) {
            // keep value as string since its between single or double quotes
            return value.substring(1, value.length() - 1);
        } else if (first == '@' && (record = Longs.tryParse(value.substring(1, value.length()))) != null) {
            return Link.to(record);
        } else if (first == '@' && last == '@' && STRING_RESOLVABLE_LINK_REGEX.matcher(value).matches()) {
            String ccl = value.substring(1, value.length() - 1);
            return ResolvableLink.create(ccl);

        } else if (value.equalsIgnoreCase("true")) {
            return true;
        } else if (value.equalsIgnoreCase("false")) {
            return false;
        } else if (first == '`' && last == '`') {
            return Tag.create(value.substring(1, value.length() - 1));
        } else {
            return MoreObjects.firstNonNull(Strings.tryParseNumber(value), value);
        }
    }

    /**
     * Convert the {@code symbol} into the appropriate {@link Operator}.
     * 
     * @param symbol - the string form of a symbol (i.e. =, >, >=, etc) or a
     *            CaSH shortcut (i.e. eq, gt, gte, etc)
     * @return the {@link Operator} that is parsed from the string
     *         {@code symbol}
     */
    public static Operator stringToOperator(String symbol) {
        switch (symbol.toLowerCase()) {
        case "==":
        case "=":
        case "eq":
            return Operator.EQUALS;
        case "!=":
        case "ne":
            return Operator.NOT_EQUALS;
        case ">":
        case "gt":
            return Operator.GREATER_THAN;
        case ">=":
        case "gte":
            return Operator.GREATER_THAN_OR_EQUALS;
        case "<":
        case "lt":
            return Operator.LESS_THAN;
        case "<=":
        case "lte":
            return Operator.LESS_THAN_OR_EQUALS;
        case "><":
        case "bw":
            return Operator.BETWEEN;
        case "->":
        case "lnk2":
        case "lnks2":
            return Operator.LINKS_TO;
        case "regex":
            return Operator.REGEX;
        case "nregex":
            return Operator.NOT_REGEX;
        case "like":
            return Operator.LIKE;
        case "nlike":
            return Operator.NOT_LIKE;
        default:
            throw new IllegalStateException("Cannot parse " + symbol + " into an operator");
        }
    }

    /**
     * <p>
     * Users are encouraged to use {@link Link#toWhere(String)} instead of this
     * method.
     * </p>
     * <p>
     * <strong>USE WITH CAUTION: </strong> This conversation is only necessary
     * when bulk inserting data in string form (i.e. importing data from a CSV
     * file) that should have static links dynamically resolved.<strong>
     * <em>Unless you are certain otherwise, you should never need to use this 
     * method because there is probably some intermediate function or framework 
     * that does this for you!</em></strong>
     * </p>
     * <p>
     * Convert the {@code ccl} string to a {@link ResolvableLink} instruction
     * for the receiver to add links to all the records that match the criteria.
     * </p>
     * <p>
     * Please note that this method only returns a specification and not an
     * actual {@link ResolvableLink} object. Use the
     * {@link #stringToJava(String)} method on the value returned from this
     * method to get the object.
     * </p>
     * 
     * @param ccl - The criteria to use when resolving link targets
     * @return An instruction to create a {@link ResolvableLink}
     */
    public static String stringToResolvableLinkInstruction(String ccl) {
        return Strings.joinSimple(RAW_RESOLVABLE_LINK_SYMBOL_PREPEND, ccl, RAW_RESOLVABLE_LINK_SYMBOL_APPEND);
    }

    /**
     * <p>
     * <strong>USE WITH CAUTION: </strong> This conversation is only necessary
     * for applications that import raw data but cannot use the Concourse API
     * directly and therefore cannot explicitly add links (e.g. the
     * import-framework that handles raw string data). <strong>
     * <em>If you have access to the Concourse API, you should not use this 
     * method!</em> </strong>
     * </p>
     * Convert the {@code rawValue} into a {@link ResolvableLink} specification
     * that instructs the receiver to add a link to all the records that have
     * {@code rawValue} mapped from {@code key}.
     * <p>
     * Please note that this method only returns a specification and not an
     * actual {@link ResolvableLink} object. Use the
     * {@link #stringToJava(String)} method on the value returned from this
     * method to get the object.
     * </p>
     * 
     * @param key
     * @param rawValue
     * @return the transformed value.
     */
    @Deprecated
    public static String stringToResolvableLinkSpecification(String key, String rawValue) {
        return stringToResolvableLinkInstruction(Strings.joinWithSpace(key, "=", rawValue));
    }

    /**
     * Return the Java Object that represents {@code object}.
     * 
     * @param object
     * @return the Object
     */
    public static Object thriftToJava(TObject object) {
        Object java = object.getJavaFormat();
        if (java == null) {
            ByteBuffer buffer = object.bufferForData();
            switch (object.getType()) {
            case BOOLEAN:
                java = ByteBuffers.getBoolean(buffer);
                break;
            case DOUBLE:
                java = buffer.getDouble();
                break;
            case FLOAT:
                java = buffer.getFloat();
                break;
            case INTEGER:
                java = buffer.getInt();
                break;
            case LINK:
                java = Link.to(buffer.getLong());
                break;
            case LONG:
                java = buffer.getLong();
                break;
            case TAG:
                java = ByteBuffers.getString(buffer);
                break;
            case NULL:
                java = null;
                break;
            default:
                java = ByteBuffers.getString(buffer);
                break;
            }
            buffer.rewind();
        }
        return java;
    }

    /**
     * In-place implementation for converting a collection of java objects to a
     * typed {@code output} collection of TObjects.
     * 
     * @param input the original collection to convert
     * @param output the output collection into which the converted objects are
     *            placed
     */
    private static void javaCollectionToThrift(Collection<Object> input, Collection<TObject> output) {
        for (Object elt : input) {
            output.add(javaToThrift(elt));
        }
    }

    /**
     * Convert the next JSON object in the {@code reader} to a mapping that
     * associates each key with the Java objects that represent the
     * corresponding values.
     * 
     * <p>
     * This method has the same rules and limitations as
     * {@link #jsonToJava(String)}. It simply uses a {@link JsonReader} to
     * handle reading an array of objects.
     * </p>
     * <p>
     * <strong>This method DOES NOT {@link JsonReader#close()} the
     * {@code reader}.</strong>
     * </p>
     * 
     * @param reader the {@link JsonReader} that contains a stream of JSON
     * @return the JSON data in the form of a {@link Multimap} from keys to
     *         values
     */
    private static Multimap<String, Object> jsonToJava(JsonReader reader) {
        Multimap<String, Object> data = HashMultimap.create();
        try {
            reader.beginObject();
            JsonToken peek0;
            while ((peek0 = reader.peek()) != JsonToken.END_OBJECT) {
                String key = reader.nextName();
                peek0 = reader.peek();
                if (peek0 == JsonToken.BEGIN_ARRAY) {
                    // If we have an array, add the elements individually. If
                    // there are any duplicates in the array, they will be
                    // filtered out by virtue of the fact that a HashMultimap
                    // does not store dupes.
                    reader.beginArray();
                    JsonToken peek = reader.peek();
                    do {
                        Object value;
                        if (peek == JsonToken.BOOLEAN) {
                            value = reader.nextBoolean();
                        } else if (peek == JsonToken.NUMBER) {
                            value = stringToJava(reader.nextString());
                        } else if (peek == JsonToken.STRING) {
                            String orig = reader.nextString();
                            value = stringToJava(orig);
                            if (orig.isEmpty()) {
                                value = orig;
                            }
                            // If the token looks like a string, it MUST be
                            // converted to a Java string unless it is a
                            // masquerading double or an instance of Thrift
                            // translatable class that has a special string
                            // representation (i.e. Tag, Link)
                            else if (orig.charAt(orig.length() - 1) != 'D'
                                    && !CLASSES_WITH_ENCODED_STRING_REPR.contains(value.getClass())) {
                                value = value.toString();
                            }
                        } else if (peek == JsonToken.NULL) {
                            reader.skipValue();
                            continue;
                        } else {
                            throw new JsonParseException("Cannot parse nested object or array within an array");
                        }
                        data.put(key, value);
                    } while ((peek = reader.peek()) != JsonToken.END_ARRAY);
                    reader.endArray();
                } else {
                    Object value;
                    if (peek0 == JsonToken.BOOLEAN) {
                        value = reader.nextBoolean();
                    } else if (peek0 == JsonToken.NUMBER) {
                        value = stringToJava(reader.nextString());
                    } else if (peek0 == JsonToken.STRING) {
                        String orig = reader.nextString();
                        value = stringToJava(orig);
                        if (orig.isEmpty()) {
                            value = orig;
                        }
                        // If the token looks like a string, it MUST be
                        // converted to a Java string unless it is a
                        // masquerading double or an instance of Thrift
                        // translatable class that has a special string
                        // representation (i.e. Tag, Link)
                        else if (orig.charAt(orig.length() - 1) != 'D'
                                && !CLASSES_WITH_ENCODED_STRING_REPR.contains(value.getClass())) {
                            value = value.toString();
                        }
                    } else if (peek0 == JsonToken.NULL) {
                        reader.skipValue();
                        continue;
                    } else {
                        throw new JsonParseException("Cannot parse nested object to value");
                    }
                    data.put(key, value);
                }
            }
            reader.endObject();
            return data;
        } catch (IOException | IllegalStateException e) {
            throw new JsonParseException(e.getMessage());
        }
    }

    /**
     * The component of a resolvable link symbol that comes after the
     * resolvable key specification in the raw data.
     */
    @PackagePrivate
    static final String RAW_RESOLVABLE_LINK_SYMBOL_APPEND = "@"; // visible
                                                                 // for
                                                                 // testing

    /**
     * The component of a resolvable link symbol that comes before the
     * resolvable key specification in the raw data.
     */
    @PackagePrivate
    static final String RAW_RESOLVABLE_LINK_SYMBOL_PREPEND = "@"; // visible
                                                                  // for
                                                                  // testing

    /**
     * These classes have a special encoding that signals that string value
     * should actually be converted to those instances in
     * {@link #jsonToJava(JsonReader)}.
     */
    @SuppressWarnings("unchecked")
    private static Set<Class<?>> CLASSES_WITH_ENCODED_STRING_REPR = Sets.newHashSet(Link.class, Tag.class,
            ResolvableLink.class);

    /**
     * A {@link Pattern} that can be used to determine whether a string matches
     * the expected pattern of an instruction to insert links to records that
     * are resolved by finding matches to a criteria.
     */
    // NOTE: This REGEX enforces that the string must contain at least one
    // space, which means that a CCL string can only be considered valid if it
    // contains a space (e.g. name=jeff is not valid CCL).
    private static final Pattern STRING_RESOLVABLE_LINK_REGEX = Pattern.compile("^@(?=.*[ ]).+@$");

    private Convert() {
        /* Utility Class */}

    /**
     * A special class that is used to indicate that the record(s) to which one
     * or more {@link Link links} should point must be resolved by finding all
     * records that match a criteria.
     * <p>
     * This class is NOT part of the public API, so it should not be used as a
     * value for input to the client. Objects of this class exist merely to
     * provide utilities that depend on the client with instructions for
     * resolving a link in cases when the end-user of the utility cannot use the
     * client directly themselves (i.e. specifying a resolvable link in a raw
     * text file for the import framework).
     * </p>
     * <p>
     * To get an object of this class, call {@link Convert#stringToJava(String)}
     * on the result of calling
     * {@link Convert#stringToResolvableLinkInstruction(String)} on the raw
     * data.
     * </p>
     * 
     * @author Jeff Nelson
     */
    @Immutable
    public static final class ResolvableLink {

        // NOTE: This class does not define #hashCode() or #equals() because the
        // defaults are the desired behaviour

        /**
         * Create a new {@link ResolvableLink} that provides instructions to
         * create links to all the records that match the {@code ccl} string.
         * 
         * @param ccl - The criteria to use when resolving link targets
         * @return the ResolvableLink
         */
        @PackagePrivate
        static ResolvableLink create(String ccl) {
            return new ResolvableLink(ccl);
        }

        /**
         * Create a new {@link ResolvableLink} that provides instructions to
         * create a link to the records which contain {@code value} for
         * {@code key}.
         * 
         * @param key
         * @param value
         * @return the ResolvableLink
         */
        @PackagePrivate
        @Deprecated
        static ResolvableLink newResolvableLink(String key, Object value) {
            return new ResolvableLink(key, value);
        }

        @Deprecated
        protected final String key;

        @Deprecated
        protected final Object value;

        /**
         * The CCL string that should be used when resolving the link targets.
         */
        private final String ccl;

        /**
         * Construct a new instance.
         * 
         * @param ccl - The criteria to use when resolving link targets
         */
        private ResolvableLink(String ccl) {
            this.ccl = ccl;
            this.key = null;
            this.value = null;
        }

        /**
         * Construct a new instance.
         * 
         * @param key
         * @param value
         * @deprecated As of version 0.5.0
         */
        @Deprecated
        private ResolvableLink(String key, Object value) {
            this.ccl = new StringBuilder().append(key).append(" = ").append(value).toString();
            this.key = key;
            this.value = value;
        }

        /**
         * Return the {@code ccl} string that should be used for resolving link
         * targets.
         * 
         * @return {@link #ccl}
         */
        public String getCcl() {
            return ccl;
        }

        /**
         * Return the associated key.
         * 
         * @return the key
         */
        @Deprecated
        public String getKey() {
            return key;
        }

        /**
         * Return the associated value.
         * 
         * @return the value
         */
        @Deprecated
        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return Strings.format("{} for {}", this.getClass().getSimpleName(), ccl);
        }

    }

}