com.cinchapi.concourse.server.http.EndpointContainer.java Source code

Java tutorial

Introduction

Here is the source code for com.cinchapi.concourse.server.http.EndpointContainer.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.server.http;

import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.List;

import com.cinchapi.common.base.AdHocIterator;
import com.cinchapi.concourse.server.ConcourseServer;
import com.cinchapi.concourse.util.Random;
import com.cinchapi.concourse.util.Reflection;
import com.cinchapi.concourse.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CaseFormat;

/**
 * An {@link EndpointContainer} is one that exposes functionality via HTTP
 * {@link Endpoint endpoints}.
 * 
 * <p>
 * The name of the EndpointContainer is used for determining the absolute path
 * to prepend to the relative paths defined for each {@link Endpoint}. The name
 * of the class is converted from upper camelcase to lowercase where each word
 * boundary is separated by a forward slash (/) and the words "Router" and
 * "Index" are stripped.
 * </p>
 * <p>
 * For example, a class named {@code com.company.module.HelloWorldRouter} will
 * have each of its {@link Endpoint endpoints} prepended with
 * {@code /com/company/module/hello/world/}.
 * <p>
 * <p>
 * {@link Endpoint Endpoints} are defined in an EndpointContainer using instance
 * variables. The name of the variable is used to determine the relative path of
 * the endpoint. For example, an Endpoint instance variable named
 * {@code get$Arg1Foo$Arg2} corresponds to the path {@code GET /:arg1/foo/:arg2}
 * relative to the path defined by the EndpointContainer. Each endpoint must
 * respond to one of the HTTP verbs (GET, POST, PUT, DELETE) and serve some
 * payload.
 * <p>
 * You may define multiple endpoints that process the same URI as long as each
 * one responds to a different HTTP verb (i.e. you may have GET /path/to/foo and
 * POST /path/to/foo).
 * </p>
 * 
 * @author Jeff Nelson
 */
public abstract class EndpointContainer implements Comparable<EndpointContainer> {

    /**
     * Given a list of arguments (as defined by the spec for declaring
     * {@link Endpoint} objects), generate a string that can use to create
     * the appropriate Spark routes.
     * 
     * @param args a list of arguments
     * @return the path to {@link Endpoint#setPath(String) assign}
     */
    @VisibleForTesting
    protected static String buildSparkPath(List<String> args) {
        if (args.isEmpty()) {
            return "";
        } else {
            boolean var = false;
            StringBuilder sb = new StringBuilder();
            for (String component : args) {
                if (component.equals("$")) {
                    var = true;
                    continue;
                }
                sb.append("/");
                if (var) {
                    sb.append(":");
                    var = false;
                }
                sb.append(component.toLowerCase());
            }
            sb.deleteCharAt(0);
            return sb.toString();
        }
    }

    /**
     * Given a fully qualified class {@code name}, return the canonical
     * namespace that should be used as a prefix when referring to the
     * container's
     * classes.
     * 
     * <p>
     * The namespace is instrumental for properly constructing the URI where the
     * container's functionality lives.
     * </p>
     * 
     * @param name a fully qualified class name
     * @return the canonical namespace to use when constructing the URI
     */
    @VisibleForTesting
    protected static String getCanonicalNamespace(String name) {
        return getCanonicalNamespace(RoutingKey.forName(name));
    }

    /**
     * Given a {@link RoutingKey}, return the canonical namespace that should be
     * used as a prefix when referring to the container's classes.
     * 
     * <p>
     * The namespace is instrumental for properly constructing the URI where the
     * container's functionality lives.
     * </p>
     * 
     * @param id a {@link RoutingKey}
     * @return the canonical namespace to use when constructing the URI
     */
    private static String getCanonicalNamespace(RoutingKey id) {
        String namespace;
        if (id.group.equals("com.cinchapi")) {
            if (id.module.equals("server")) {
                namespace = id.cls;
            } else {
                namespace = Strings.join('/', id.module, id.cls);
            }
        } else {
            namespace = Strings.join('/', id.group, id.module, id.cls);
        }
        namespace = namespace.replace("Router", "");
        namespace = namespace.replace("Index", "");
        namespace = namespace.replaceAll("\\.", "/");
        namespace = namespace.replaceAll("_", "/");
        namespace = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, namespace);
        namespace = namespace.replaceAll("/-", "/");
        namespace = Strings.ensureStartsWith(namespace, "/");
        namespace = Strings.ensureEndsWith(namespace, "/");
        return namespace;
    }

    /**
     * A reference to the {@link ConcourseServer backend} where this container
     * is
     * registered.
     */
    protected final ConcourseServer concourse;

    /**
     * The namespace is prepended to the relative paths for every
     * {@link Endpoint}.
     */
    protected final String namespace = getCanonicalNamespace(getRoutingKey());

    /**
     * Construct a new instance.
     * 
     * @param concourse
     */
    protected EndpointContainer(ConcourseServer concourse) {
        this.concourse = concourse;
    }

    @Override
    public int compareTo(EndpointContainer other) {
        int p0 = getWeight();
        int p1 = other.getWeight();
        if (this == other) {
            return 0;
        } else if (p0 == p1) {
            int c = getClass().getSimpleName().compareTo(other.getClass().getSimpleName());
            if (c != 0) {
                return c;
            } else {
                // If all other comparisons indicate that the containers have
                // the same weight and same name, then just randomly sort them.
                return Random.getInt() % 2 == 0 ? 1 : -1;
            }
        } else {
            return p0 > p1 ? 1 : -1;
        }
    }

    /**
     * Return an {@link Iterable iterable} collection of all the
     * {@link Endpoint endpoints} that are defined in this container.
     * 
     * @return all the defined endpoints
     */
    public final Iterable<Endpoint> endpoints() {
        return new Iterable<Endpoint>() {

            private final Field[] fields = EndpointContainer.this.getClass().getDeclaredFields();
            private int i = 0;

            @Override
            public Iterator<Endpoint> iterator() {
                return new AdHocIterator<Endpoint>() {

                    @Override
                    protected Endpoint findNext() {
                        if (i >= fields.length) {
                            return null;
                        } else {
                            Field field = fields[i];
                            String name = field.getName();
                            ++i;
                            if (Endpoint.class.isAssignableFrom(field.getType())) {
                                Endpoint callable = Reflection.getCasted(field, EndpointContainer.this);
                                String action = callable.getAction();
                                String path = callable.getPath();
                                if (action == null && path == null
                                        && (name.startsWith("get") || name.startsWith("post")
                                                || name.startsWith("put") || name.startsWith("delete")
                                                || name.startsWith("upsert") || name.startsWith("options"))) {
                                    List<String> args = Strings.splitCamelCase(field.getName());
                                    action = args.remove(0);
                                    path = buildSparkPath(args);
                                }
                                path = Strings.joinSimple(namespace, path);
                                Reflection.set("action", action, callable);
                                Reflection.set("path", path, callable);
                                return callable;
                            } else {
                                return findNext();
                            }
                        }
                    }
                };
            }
        };
    }

    /**
     * Return the appropriate {@link RoutingKey}. Subclasses may wish to
     * override
     * to provide custom naming functionality.
     * 
     * @return the {@link RoutingKey}
     */
    protected final RoutingKey getRoutingKey() {
        return RoutingKey.forClass(this.getClass());
    }

    /**
     * Return the relative weight for this {@link EndpointContainer container}.
     * Weights
     * are used to determine the order in which {@link EndpointContainer
     * containers} from
     * the same bundle are weighted. This is important because routes are
     * matched in the order in which they are registered.
     * 
     * <p>
     * A larger weight means that the routes herwithin will be registered later
     * (e.g. larger weights sink to the bottom). {@link EndpointContainer
     * Plugins} that have the same weight are registered in a random order that
     * may change between JVM invocations.
     * </p>
     * 
     * @return the container weight
     */
    protected int getWeight() {
        return 0;
    }

}