Java tutorial
/* * Copyright Myrrix Ltd * * 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 net.myrrix.web.servlets; import java.io.IOException; import java.io.Writer; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.Maps; import com.google.common.net.HostAndPort; import com.google.common.net.HttpHeaders; import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; import org.apache.mahout.cf.taste.recommender.RecommendedItem; import net.myrrix.common.LangUtils; import net.myrrix.common.MyrrixRecommender; import net.myrrix.common.ReloadingReference; import net.myrrix.common.collection.FastIDSet; import net.myrrix.common.random.RandomUtils; import net.myrrix.online.RescorerProvider; import net.myrrix.web.common.stats.ServletStats; /** * Superclass of {@link HttpServlet}s used in the application. All API methods return the following * HTTP statuses in certain situations: * * <ul> * <li>{@code 302 Temporary Redirect} if, in a distributed environment, another partition should * handle the request</li> * <li>{@code 400 Bad Request} if the arguments are invalid, like a non-numeric ID</li> * <li>{@code 401 Unauthorized} if a username/password is required, but not supplied correctly * in the request via HTTP DIGEST</li> * <li>{@code 405 Method Not Allowed} if an incorrect HTTP method is used, like {@code GET} * where {@code POST} is required</li> * <li>{@code 500 Internal Server Error} if an unexpected server-side exception occurs</li> * <li>{@code 503 Service Unavailable} if no model is yet available to serve requests</li> * </ul> * * @author Sean Owen * @since 1.0 */ public abstract class AbstractMyrrixServlet extends HttpServlet { private static final Splitter COMMA = Splitter.on(',').omitEmptyStrings().trimResults(); static final Splitter SLASH = Splitter.on('/').omitEmptyStrings(); static final int DEFAULT_HOW_MANY = 10; private static final String KEY_PREFIX = AbstractMyrrixServlet.class.getName(); public static final String READ_ONLY_KEY = KEY_PREFIX + ".READ_ONLY"; public static final String RECOMMENDER_KEY = KEY_PREFIX + ".RECOMMENDER"; public static final String RESCORER_PROVIDER_KEY = KEY_PREFIX + ".RESCORER_PROVIDER"; public static final String TIMINGS_KEY = KEY_PREFIX + ".TIMINGS"; public static final String LOCAL_INPUT_DIR_KEY = KEY_PREFIX + ".LOCAL_INPUT_DIR"; public static final String ALL_PARTITIONS_REF_KEY = KEY_PREFIX + ".ALL_PARTITIONS"; public static final String PARTITION_KEY = KEY_PREFIX + ".PARTITION"; private static final String[] NO_PARAMS = new String[0]; private MyrrixRecommender recommender; private RescorerProvider rescorerProvider; private ServletStats timing; private ReloadingReference<List<List<HostAndPort>>> allPartitions; private int thisPartition; private ConcurrentMap<String, ResponseContentType> responseTypeCache; @Override public void init(ServletConfig config) throws ServletException { super.init(config); ServletContext context = config.getServletContext(); recommender = (MyrrixRecommender) context.getAttribute(RECOMMENDER_KEY); rescorerProvider = (RescorerProvider) context.getAttribute(RESCORER_PROVIDER_KEY); @SuppressWarnings("unchecked") ReloadingReference<List<List<HostAndPort>>> theAllPartitions = (ReloadingReference<List<List<HostAndPort>>>) context .getAttribute(ALL_PARTITIONS_REF_KEY); allPartitions = theAllPartitions; thisPartition = (Integer) context.getAttribute(PARTITION_KEY); responseTypeCache = Maps.newConcurrentMap(); Map<String, ServletStats> timings; synchronized (context) { @SuppressWarnings("unchecked") Map<String, ServletStats> temp = (Map<String, ServletStats>) context.getAttribute(TIMINGS_KEY); timings = temp; if (timings == null) { timings = Maps.newTreeMap(); context.setAttribute(TIMINGS_KEY, timings); } } String key = getClass().getSimpleName(); ServletStats theTiming = timings.get(key); if (theTiming == null) { theTiming = new ServletStats(); timings.put(key, theTiming); } timing = theTiming; } @Override protected final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (allPartitions != null) { List<List<HostAndPort>> thePartitions = allPartitions.get(1, TimeUnit.SECONDS); Long unnormalizedPartitionToServe = getUnnormalizedPartitionToServe(request); if (unnormalizedPartitionToServe != null) { int partitionToServe = LangUtils.mod(unnormalizedPartitionToServe, thePartitions.size()); if (partitionToServe != thisPartition) { String redirectURL = buildRedirectToPartitionURL(request, partitionToServe, unnormalizedPartitionToServe, thePartitions); response.sendRedirect(redirectURL); return; } } // else, we are the right partition to serve or no specific partition needed, so continue // Note that the client will send any traffic that does not require a particular partition to partition 0. // This logic just says that any partition is happy to try to answer such a request rather than forward it // to partition 0, but, it is a difference in behavior. } long start = System.nanoTime(); super.service(request, response); timing.addTimingNanosec(System.nanoTime() - start); int status = response.getStatus(); if (status >= 400) { if (status >= 500) { timing.incrementServerErrors(); } else { timing.incrementClientErrors(); } } } private static String buildRedirectToPartitionURL(HttpServletRequest request, int toPartition, long unnormalizedPartitionToServe, List<List<HostAndPort>> thePartitions) { List<HostAndPort> replicas = thePartitions.get(toPartition); // Also determine (first) replica by hashing to preserve a predictable order of access // through the replicas for a given ID int chosenReplica = LangUtils.mod(RandomUtils.md5HashToLong(unnormalizedPartitionToServe), replicas.size()); HostAndPort hostPort = replicas.get(chosenReplica); StringBuilder redirectURL = new StringBuilder(); redirectURL.append(request.isSecure() ? "https" : "http").append("://"); redirectURL.append(hostPort.getHostText()).append(':').append(hostPort.getPort()); redirectURL.append(request.getRequestURI()); String query = request.getQueryString(); if (query != null) { redirectURL.append('?').append(query); } return redirectURL.toString(); } /** * @param request request containing info that may determine which partition needs to serve it * @return {@code null} if any partition may serve, or an integral value that should be used to * determine the partiiton. This is usually an ID value, which will be possibly hashed and * reduced modulo the number of partitions. */ protected Long getUnnormalizedPartitionToServe(HttpServletRequest request) { return null; // Default: any partition is OK } protected final MyrrixRecommender getRecommender() { return recommender; } protected final RescorerProvider getRescorerProvider() { return rescorerProvider; } /** * @return timing information for requests directed at this servlet instance */ public final ServletStats getTiming() { return timing; } protected static int getHowMany(ServletRequest request) { String howManyString = request.getParameter("howMany"); if (howManyString == null) { return DEFAULT_HOW_MANY; } int howMany = Integer.parseInt(howManyString); Preconditions.checkArgument(howMany > 0, "howMany must be positive"); return howMany; } protected static String[] getRescorerParams(ServletRequest request) { String[] rescorerParams = request.getParameterValues("rescorerParams"); return rescorerParams == null ? NO_PARAMS : rescorerParams; } protected static boolean getConsiderKnownItems(ServletRequest request) { return Boolean.valueOf(request.getParameter("considerKnownItems")); } /** * <p>Outputs items in CSV or JSON format based on the HTTP {@code Accept} header.</p> * * <p>CSV output contains one recommendation per line, and each line is of the form {@code itemID, strength}, * like {@code 325, 0.53}. Strength is an opaque indicator of the relative quality of the recommendation.</p> * * <p>JSON output is an array of arrays, with each sub-array containing an item ID and strength. * Example: {@code [[325, 0.53], [98, 0.499]]}.</p> */ protected final void output(HttpServletRequest request, ServletResponse response, Iterable<RecommendedItem> items) throws IOException { Writer writer = response.getWriter(); switch (determineResponseType(request)) { case JSON: writer.write('['); boolean first = true; for (RecommendedItem item : items) { if (first) { first = false; } else { writer.write(','); } writer.write('['); writer.write(Long.toString(item.getItemID())); writer.write(','); writer.write(Float.toString(item.getValue())); writer.write(']'); } writer.write(']'); break; case CSV: for (RecommendedItem item : items) { writer.write(Long.toString(item.getItemID())); writer.write(','); writer.write(Float.toString(item.getValue())); writer.write('\n'); } break; default: throw new IllegalStateException("Unknown response type"); } } /** * Determines the appropriate content type for the response based on request headers. At the moment these * are chosen from the values in {@link ResponseContentType}. */ final ResponseContentType determineResponseType(HttpServletRequest request) { String acceptHeader = request.getHeader(HttpHeaders.ACCEPT); if (acceptHeader == null) { return ResponseContentType.JSON; } ResponseContentType cached = responseTypeCache.get(acceptHeader); if (cached != null) { return cached; } SortedMap<Double, ResponseContentType> types = Maps.newTreeMap(); for (String accept : COMMA.split(acceptHeader)) { double preference; String type; int semiColon = accept.indexOf(';'); if (semiColon < 0) { preference = 1.0; type = accept; } else { String valueString = accept.substring(semiColon + 1).trim(); if (valueString.startsWith("q=")) { valueString = valueString.substring(2); } try { preference = LangUtils.parseDouble(valueString); } catch (IllegalArgumentException ignored) { preference = 1.0; } type = accept.substring(semiColon); } ResponseContentType parsedType = null; if ("text/csv".equals(type) || "text/plain".equals(type)) { parsedType = ResponseContentType.CSV; } else if ("application/json".equals(type)) { parsedType = ResponseContentType.JSON; } if (parsedType != null) { types.put(preference, parsedType); } } ResponseContentType finalType; if (types.isEmpty()) { finalType = ResponseContentType.JSON; } else { finalType = types.values().iterator().next(); } responseTypeCache.putIfAbsent(acceptHeader, finalType); return finalType; } /** * Outputs IDs in CSV or JSON format. When outputting CSV, one ID is written per line. When outputting * JSON, the output is an array of IDs. */ final void outputIDs(HttpServletRequest request, ServletResponse response, FastIDSet ids) throws IOException { Writer writer = response.getWriter(); LongPrimitiveIterator it = ids.iterator(); switch (determineResponseType(request)) { case JSON: writer.write('['); boolean first = true; while (it.hasNext()) { if (first) { first = false; } else { writer.write(','); } writer.write(Long.toString(it.nextLong())); } writer.write(']'); break; case CSV: while (it.hasNext()) { writer.write(Long.toString(it.nextLong())); writer.write('\n'); } break; default: throw new IllegalStateException("Unknown response type"); } } /** * Available content types / formats for response bodies. */ enum ResponseContentType { JSON, CSV, } }