org.prebake.core.BoundName.java Source code

Java tutorial

Introduction

Here is the source code for org.prebake.core.BoundName.java

Source

// Copyright 2010, Mike Samuel
//
// 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.prebake.core;

import org.prebake.js.JsonSerializable;
import org.prebake.js.JsonSink;
import org.prebake.js.JsonSource;
import org.prebake.js.YSON;

import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.caja.lexer.escaping.Escaping;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

/**
 * A name and parameters represented as an unordered key/value map of strings.
 *
 * @author Mike Samuel <mikesamuel@gmail.com>
 */
@ParametersAreNonnullByDefault
public final class BoundName implements Comparable<BoundName>, JsonSerializable {
    /** Canonical. */
    @Nonnull
    public final String ident;
    /** Keys sorted. */
    @Nonnull
    public final ImmutableMap<String, String> bindings;

    private BoundName(String ident, ImmutableMap<String, String> bindings) {
        this.ident = canonIdent(ident, bindings);
        this.bindings = bindings;
    }

    public void toJson(JsonSink sink) throws IOException {
        sink.writeValue(ident);
    }

    public int compareTo(BoundName that) {
        return this.ident.compareTo(that.ident);
    }

    @Override
    public boolean equals(Object o) {
        return o != null && o.getClass() == getClass() && this.ident.equals(((BoundName) o).ident);
    }

    @Override
    public int hashCode() {
        return ident.hashCode();
    }

    @Override
    public String toString() {
        return ident;
    }

    public BoundName withBindings(Map<String, String> bindings) {
        ImmutableMap<String, String> newBindings = ImmutableMap.copyOf(new TreeMap<String, String>(bindings));
        return new BoundName(getRawIdent(), newBindings);
    }

    public String getRawIdent() {
        return bindings.isEmpty() ? ident : ident.substring(0, ident.indexOf('['));
    }

    /**
     * Converts a string like {@code "foo"} or {@code "bar[\"x\":\"y\"]"} into
     * a bound identifier.
     * <p>
     * This factory function has the signature that
     * {@link org.prebake.js.YSONConverter.Factory#withType} expects.
     */
    public static BoundName fromString(String s) {
        int lbracket = s.indexOf('[');
        String ident;
        ImmutableMap<String, String> bindings;
        if (lbracket < 0) {
            ident = s;
            // Optimization for common case.
            bindings = ImmutableMap.of();
        } else {
            ident = s.substring(0, lbracket);
            JsonSource src = new JsonSource(new StringReader(s.substring(lbracket + 1)));
            try {
                if (src.check("]")) {
                    bindings = ImmutableMap.of();
                } else {
                    SortedMap<String, String> b = Maps.newTreeMap();
                    do {
                        String key = src.expectString();
                        src.expect(":");
                        String value = src.expectString();
                        String old = b.put(key, value);
                        if (old != null && !old.equals(value)) {
                            throw new IllegalArgumentException("Duplicate binding " + JsonSink.stringify(key)
                                    + " with values " + JsonSink.stringify(old) + " and "
                                    + JsonSink.stringify(value) + " for " + s);
                        }
                    } while (src.check(","));
                    src.expect("]");
                    bindings = ImmutableMap.copyOf(b);
                }
            } catch (IOException ex) {
                // Propagate syntax errors as IllegalArgumentExceptions
                throw new IllegalArgumentException(ex.getMessage(), ex);
            }
        }
        if (!YSON.isValidDottedIdentifierName(ident)) {
            throw new IllegalArgumentException(s + " is not a valid identifier");
        }
        return new BoundName(ident, bindings);
    }

    private static String canonIdent(String ident, Map<String, String> sortedBindings) {
        if (sortedBindings.isEmpty()) {
            return ident;
        }
        StringBuilder sb = new StringBuilder(ident.length() + 16 * sortedBindings.size());
        sb.append(ident);
        char sep = '[';
        for (Map.Entry<String, String> binding : sortedBindings.entrySet()) {
            sb.append(sep).append('"');
            Escaping.escapeJsString(binding.getKey(), false, false, sb);
            sb.append("\":\"");
            Escaping.escapeJsString(binding.getValue(), false, false, sb);
            sb.append('"');

            sep = ',';
        }
        return sb.append(']').toString();
    }
}