| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.hadoop.yarn.webapp; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import java.io.IOException; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.hadoop.classification.InterfaceAudience; |
| import org.apache.hadoop.http.HtmlQuoting; |
| import org.apache.hadoop.yarn.webapp.Controller.RequestContext; |
| import org.apache.hadoop.yarn.webapp.Router.Dest; |
| import org.apache.hadoop.yarn.webapp.view.ErrorPage; |
| import org.apache.hadoop.yarn.webapp.view.RobotsTextPage; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.collect.Iterables; |
| import com.google.inject.Inject; |
| import com.google.inject.Injector; |
| import com.google.inject.Singleton; |
| |
| /** |
| * The servlet that dispatch request to various controllers |
| * according to the user defined routes in the router. |
| */ |
| @InterfaceAudience.LimitedPrivate({"YARN", "MapReduce"}) |
| @Singleton |
| public class Dispatcher extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| static final Logger LOG = LoggerFactory.getLogger(Dispatcher.class); |
| static final String ERROR_COOKIE = "last-error"; |
| static final String STATUS_COOKIE = "last-status"; |
| |
| private transient final Injector injector; |
| private transient final Router router; |
| private transient final WebApp webApp; |
| private volatile boolean devMode = false; |
| |
| @Inject |
| Dispatcher(WebApp webApp, Injector injector, Router router) { |
| this.webApp = webApp; |
| this.injector = injector; |
| this.router = router; |
| } |
| |
| @Override |
| public void doOptions(HttpServletRequest req, HttpServletResponse res) { |
| // for simplicity |
| res.setHeader("Allow", "GET, POST"); |
| } |
| |
| @Override |
| public void service(HttpServletRequest req, HttpServletResponse res) |
| throws ServletException, IOException { |
| res.setCharacterEncoding("UTF-8"); |
| String uri = HtmlQuoting.quoteHtmlChars(req.getRequestURI()); |
| |
| if (uri == null) { |
| uri = "/"; |
| } |
| if (devMode && uri.equals("/__stop")) { |
| // quick hack to restart servers in dev mode without OS commands |
| res.setStatus(res.SC_NO_CONTENT); |
| LOG.info("dev mode restart requested"); |
| prepareToExit(); |
| return; |
| } |
| // if they provide a redirectPath go there instead of going to |
| // "/" so that filters can differentiate the webapps. |
| if (uri.equals("/")) { |
| String redirectPath = webApp.getRedirectPath(); |
| if (redirectPath != null && !redirectPath.isEmpty()) { |
| res.sendRedirect(redirectPath); |
| return; |
| } |
| } |
| String method = req.getMethod(); |
| if (method.equals("OPTIONS")) { |
| doOptions(req, res); |
| return; |
| } |
| if (method.equals("TRACE")) { |
| doTrace(req, res); |
| return; |
| } |
| if (method.equals("HEAD")) { |
| doGet(req, res); // default to bad request |
| return; |
| } |
| String pathInfo = req.getPathInfo(); |
| if (pathInfo == null) { |
| pathInfo = "/"; |
| } |
| Controller.RequestContext rc = |
| injector.getInstance(Controller.RequestContext.class); |
| |
| //short-circuit robots.txt serving for all YARN webapps. |
| if (uri.equals(RobotsTextPage.ROBOTS_TXT_PATH)) { |
| rc.setStatus(HttpServletResponse.SC_FOUND); |
| render(RobotsTextPage.class); |
| return; |
| } |
| |
| if (setCookieParams(rc, req) > 0) { |
| Cookie ec = rc.cookies().get(ERROR_COOKIE); |
| if (ec != null) { |
| rc.setStatus(Integer.parseInt(rc.cookies(). |
| get(STATUS_COOKIE).getValue())); |
| removeErrorCookies(res, uri); |
| rc.set(Params.ERROR_DETAILS, ec.getValue()); |
| render(ErrorPage.class); |
| return; |
| } |
| } |
| rc.prefix = webApp.name(); |
| Router.Dest dest = null; |
| try { |
| dest = router.resolve(method, pathInfo); |
| } catch (WebAppException e) { |
| rc.error = e; |
| if (!e.getMessage().contains("not found")) { |
| rc.setStatus(res.SC_INTERNAL_SERVER_ERROR); |
| render(ErrorPage.class); |
| return; |
| } |
| } |
| if (dest == null) { |
| rc.setStatus(res.SC_NOT_FOUND); |
| render(ErrorPage.class); |
| return; |
| } |
| rc.devMode = devMode; |
| setMoreParams(rc, pathInfo, dest); |
| Controller controller = injector.getInstance(dest.controllerClass); |
| try { |
| // TODO: support args converted from /path/:arg1/... |
| dest.action.invoke(controller, (Object[]) null); |
| if (!rc.rendered) { |
| if (dest.defaultViewClass != null) { |
| render(dest.defaultViewClass); |
| } else if (rc.status == 200) { |
| throw new IllegalStateException("No view rendered for 200"); |
| } |
| } |
| } catch (Exception e) { |
| LOG.error("error handling URI: "+ uri, e); |
| // Page could be half rendered (but still not flushed). So redirect. |
| redirectToErrorPage(res, e, uri, devMode); |
| } |
| } |
| |
| public static void redirectToErrorPage(HttpServletResponse res, Throwable e, |
| String path, boolean devMode) { |
| String st = devMode ? ErrorPage.toStackTrace(e, 1024 * 3) // spec: min 4KB |
| : "See logs for stack trace"; |
| res.setStatus(res.SC_FOUND); |
| Cookie cookie = new Cookie(STATUS_COOKIE, String.valueOf(500)); |
| cookie.setPath(path); |
| res.addCookie(cookie); |
| cookie = new Cookie(ERROR_COOKIE, st); |
| cookie.setPath(path); |
| res.addCookie(cookie); |
| res.setHeader("Location", path); |
| } |
| |
| public static void removeErrorCookies(HttpServletResponse res, String path) { |
| removeCookie(res, ERROR_COOKIE, path); |
| removeCookie(res, STATUS_COOKIE, path); |
| } |
| |
| public static void removeCookie(HttpServletResponse res, String name, |
| String path) { |
| LOG.debug("removing cookie {} on {}", name, path); |
| Cookie c = new Cookie(name, ""); |
| c.setMaxAge(0); |
| c.setPath(path); |
| res.addCookie(c); |
| } |
| |
| private void render(Class<? extends View> cls) { |
| injector.getInstance(cls).render(); |
| } |
| |
| // /path/foo/bar with /path/:arg1/:arg2 will set {arg1=>foo, arg2=>bar} |
| private void setMoreParams(RequestContext rc, String pathInfo, Dest dest) { |
| checkState(pathInfo.startsWith(dest.prefix), "prefix should match"); |
| if (dest.pathParams.size() == 0 || |
| dest.prefix.length() == pathInfo.length()) { |
| return; |
| } |
| String[] parts = Iterables.toArray(WebApp.pathSplitter.split( |
| pathInfo.substring(dest.prefix.length())), String.class); |
| LOG.debug("parts={}, params={}", parts, dest.pathParams); |
| for (int i = 0; i < dest.pathParams.size() && i < parts.length; ++i) { |
| String key = dest.pathParams.get(i); |
| if (key.charAt(0) == ':') { |
| rc.moreParams().put(key.substring(1), parts[i]); |
| } |
| } |
| } |
| |
| private int setCookieParams(RequestContext rc, HttpServletRequest req) { |
| Cookie[] cookies = req.getCookies(); |
| if (cookies != null) { |
| for (Cookie cookie : cookies) { |
| rc.cookies().put(cookie.getName(), cookie); |
| } |
| return cookies.length; |
| } |
| return 0; |
| } |
| |
| public void setDevMode(boolean choice) { |
| devMode = choice; |
| } |
| |
| private void prepareToExit() { |
| checkState(devMode, "only in dev mode"); |
| new Timer("webapp exit", true).schedule(new TimerTask() { |
| @Override public void run() { |
| LOG.info("WebAppp /{} exiting...", webApp.name()); |
| webApp.stop(); |
| System.exit(0); // FINDBUG: this is intended in dev mode |
| } |
| }, 18); // enough time for the last local request to complete |
| } |
| } |