Java tutorial
/* * 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; } }