Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Dispatcher.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Dispatcher.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Dispatcher.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Dispatcher.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,229 @@ +/** +* 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.*; +import com.google.common.collect.Iterables; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Singleton; + +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.yarn.webapp.Controller.RequestContext; +import org.apache.hadoop.yarn.webapp.Router.Dest; +import org.apache.hadoop.yarn.webapp.view.ErrorPage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The servlet that dispatch request to various controllers + * according to the user defined routes in the router. + */ +@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 = 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; + } + 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); + 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 + } +}
Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/MimeType.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/MimeType.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/MimeType.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/MimeType.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,28 @@ +/** +* 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; + +public interface MimeType { + + public static final String TEXT = "text/plain; charset=UTF-8"; + public static final String HTML = "text/html; charset=UTF-8"; + public static final String XML = "text/xml; charset=UTF-8"; + public static final String HTTP = "message/http; charset=UTF-8"; + public static final String JSON = "application/json; charset=UTF-8"; +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Params.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Params.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Params.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Params.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,30 @@ +/** +* 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; + +/** + * Public static constants for webapp parameters. Do NOT put any + * private or application specific constants here as they're part of + * the API for users of the controllers and views. + */ +public interface Params { + static final String TITLE = "title"; + static final String TITLE_LINK = "title.href"; + static final String ERROR_DETAILS = "error.details"; +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ResponseInfo.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ResponseInfo.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ResponseInfo.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ResponseInfo.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,87 @@ +/** +* 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 com.google.common.collect.Lists; +import com.google.inject.servlet.RequestScoped; + +import java.util.Iterator; +import java.util.List; + +/** + * A class to help passing around request scoped info + */ +@RequestScoped +public class ResponseInfo implements Iterable<ResponseInfo.Item> { + + public static class Item { + public final String key; + public final String url; + public final Object value; + + Item(String key, String url, Object value) { + this.key = key; + this.url = url; + this.value = value; + } + + public static Item of(String key, Object value) { + return new Item(key, null, value); + } + + public static Item of(String key, String url, Object value) { + return new Item(key, url, value); + } + } + + final List<Item> items = Lists.newArrayList(); + String about = "Info"; + + // Do NOT add any constructors here, unless... + + public static ResponseInfo $about(String about) { + ResponseInfo info = new ResponseInfo(); + info.about = about; + return info; + } + + public ResponseInfo about(String about) { + this.about = about; + return this; + } + + public String about() { + return about; + } + + public ResponseInfo _(String key, Object value) { + items.add(Item.of(key, value)); + return this; + } + + public ResponseInfo _(String key, String url, Object anchor) { + items.add(Item.of(key, url, anchor)); + return this; + } + + @Override + public Iterator<Item> iterator() { + return items.iterator(); + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Router.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Router.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Router.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/Router.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,286 @@ +/** +* 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 com.google.common.base.CharMatcher; +import static com.google.common.base.Preconditions.*; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; + +import static org.apache.hadoop.yarn.util.StringHelper.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages path info to controller#action routing. + */ +class Router { + static final Logger LOG = LoggerFactory.getLogger(Router.class); + static final ImmutableList<String> EMPTY_LIST = ImmutableList.of(); + static final CharMatcher SLASH = CharMatcher.is('/'); + static final Pattern controllerRe = + Pattern.compile("^/[A-Za-z_]\\w*(?:/.*)?"); + + static class Dest { + final String prefix; + final ImmutableList<String> pathParams; + final Method action; + final Class<? extends Controller> controllerClass; + Class<? extends View> defaultViewClass; + final EnumSet<WebApp.HTTP> methods; + + Dest(String path, Method method, Class<? extends Controller> cls, + List<String> pathParams, WebApp.HTTP httpMethod) { + prefix = checkNotNull(path); + action = checkNotNull(method); + controllerClass = checkNotNull(cls); + this.pathParams = pathParams != null ? ImmutableList.copyOf(pathParams) + : EMPTY_LIST; + methods = EnumSet.of(httpMethod); + } + } + + Class<?> hostClass; // starting point to look for default classes + + final TreeMap<String, Dest> routes = Maps.newTreeMap(); // path->dest + + /** + * Add a route to the router. + * e.g., add(GET, "/foo/show", FooController.class, "show", [name...]); + * The name list is from /foo/show/:name/... + */ + synchronized Dest add(WebApp.HTTP httpMethod, String path, + Class<? extends Controller> cls, + String action, List<String> names) { + LOG.debug("adding {}({})->{}#{}", new Object[]{path, names, cls, action}); + Dest dest = addController(httpMethod, path, cls, action, names); + addDefaultView(dest); + return dest; + } + + private Dest addController(WebApp.HTTP httpMethod, String path, + Class<? extends Controller> cls, + String action, List<String> names) { + for (Method method : cls.getDeclaredMethods()) { + if (method.getName().equals(action) && + method.getParameterTypes().length == 0 && + Modifier.isPublic(method.getModifiers())) { + // TODO: deal with parameters using the names + Dest dest = routes.get(path); + if (dest == null) { + method.setAccessible(true); // avoid any runtime checks + dest = new Dest(path, method, cls, names, httpMethod); + routes.put(path, dest); + return dest; + } + dest.methods.add(httpMethod); + return dest; + } + } + throw new WebAppException(action + "() not found in " + cls); + } + + private void addDefaultView(Dest dest) { + String controllerName = dest.controllerClass.getSimpleName(); + if (controllerName.endsWith("Controller")) { + controllerName = controllerName.substring(0, + controllerName.length() - 10); + } + dest.defaultViewClass = find(View.class, + dest.controllerClass.getPackage().getName(), + join(controllerName + "View")); + } + + void setHostClass(Class<?> cls) { + hostClass = cls; + } + + /** + * Resolve a path to a destination. + */ + synchronized Dest resolve(String httpMethod, String path) { + WebApp.HTTP method = WebApp.HTTP.valueOf(httpMethod); // can throw + Dest dest = lookupRoute(method, path); + if (dest == null) { + return resolveDefault(method, path); + } + return dest; + } + + private Dest lookupRoute(WebApp.HTTP method, String path) { + String key = path; + do { + Dest dest = routes.get(key); + if (dest != null && methodAllowed(method, dest)) { + if ((Object)key == path) { // shut up warnings + LOG.debug("exact match for {}: {}", key, dest.action); + return dest; + } else if (isGoodMatch(dest, path)) { + LOG.debug("prefix match2 for {}: {}", key, dest.action); + return dest; + } + return resolveAction(method, dest, path); + } + Map.Entry<String, Dest> lower = routes.lowerEntry(key); + if (lower == null) { + return null; + } + dest = lower.getValue(); + if (prefixMatches(dest, path)) { + if (methodAllowed(method, dest)) { + if (isGoodMatch(dest, path)) { + LOG.debug("prefix match for {}: {}", lower.getKey(), dest.action); + return dest; + } + return resolveAction(method, dest, path); + } + // check other candidates + int slashPos = key.lastIndexOf('/'); + key = slashPos > 0 ? path.substring(0, slashPos) : "/"; + } else { + key = "/"; + } + } while (true); + } + + static boolean methodAllowed(WebApp.HTTP method, Dest dest) { + // Accept all methods by default, unless explicity configured otherwise. + return dest.methods.contains(method) || (dest.methods.size() == 1 && + dest.methods.contains(WebApp.HTTP.GET)); + } + + static boolean prefixMatches(Dest dest, String path) { + LOG.debug("checking prefix {}{} for path: {}", new Object[]{dest.prefix, + dest.pathParams, path}); + if (!path.startsWith(dest.prefix)) { + return false; + } + int prefixLen = dest.prefix.length(); + if (prefixLen > 1 && path.length() > prefixLen && + path.charAt(prefixLen) != '/') { + return false; + } + // prefix is / or prefix is path or prefix/... + return true; + } + + static boolean isGoodMatch(Dest dest, String path) { + if (SLASH.countIn(dest.prefix) > 1) { + return true; + } + // We want to match (/foo, :a) for /foo/bar/blah and (/, :a) for /123 + // but NOT / for /foo or (/, :a) for /foo or /foo/ because default route + // (FooController#index) for /foo and /foo/ takes precedence. + if (dest.prefix.length() == 1) { + return dest.pathParams.size() > 0 && !maybeController(path); + } + return dest.pathParams.size() > 0 || // /foo should match /foo/ + (path.endsWith("/") && SLASH.countIn(path) == 2); + } + + static boolean maybeController(String path) { + return controllerRe.matcher(path).matches(); + } + + // Assume /controller/action style path + private Dest resolveDefault(WebApp.HTTP method, String path) { + List<String> parts = WebApp.parseRoute(path); + String controller = parts.get(WebApp.R_CONTROLLER); + String action = parts.get(WebApp.R_ACTION); + // NameController is encouraged default + Class<? extends Controller> cls = find(Controller.class, + join(controller, "Controller")); + if (cls == null) { + cls = find(Controller.class, controller); + } + if (cls == null) { + throw new WebAppException(join(path, ": controller for ", controller, + " not found")); + } + return add(method, defaultPrefix(controller, action), cls, action, null); + } + + private String defaultPrefix(String controller, String action) { + if (controller.equals("default") && action.equals("index")) { + return "/"; + } + if (action.equals("index")) { + return join('/', controller); + } + return pjoin("", controller, action); + } + + private <T> Class<? extends T> find(Class<T> cls, String cname) { + String pkg = hostClass.getPackage().getName(); + return find(cls, pkg, cname); + } + + private <T> Class<? extends T> find(Class<T> cls, String pkg, String cname) { + String name = StringUtils.capitalize(cname); + Class<? extends T> found = load(cls, djoin(pkg, name)); + if (found == null) { + found = load(cls, djoin(pkg, "webapp", name)); + } + if (found == null) { + found = load(cls, join(hostClass.getName(), '$', name)); + } + return found; + } + + @SuppressWarnings("unchecked") + private <T> Class<? extends T> load(Class<T> cls, String className) { + LOG.debug("trying: {}", className); + try { + Class<?> found = Class.forName(className); + if (cls.isAssignableFrom(found)) { + LOG.debug("found {}", className); + return (Class<? extends T>) found; + } + LOG.warn("found a {} but it's not a {}", className, cls.getName()); + } catch (ClassNotFoundException e) { + // OK in this case. + } + return null; + } + + // Dest may contain a candidate controller + private Dest resolveAction(WebApp.HTTP method, Dest dest, String path) { + if (dest.prefix.length() == 1) { + return null; + } + checkState(!isGoodMatch(dest, path), dest.prefix); + checkState(SLASH.countIn(path) > 1, path); + List<String> parts = WebApp.parseRoute(path); + String controller = parts.get(WebApp.R_CONTROLLER); + String action = parts.get(WebApp.R_ACTION); + return add(method, pjoin("", controller, action), dest.controllerClass, + action, null); + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/SubView.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/SubView.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/SubView.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/SubView.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,29 @@ +/** +* 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; + +/** + * Interface for SubView to avoid top-level inclusion + */ +public interface SubView { + /** + * render the sub-view + */ + void renderPartial(); +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ToJSON.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ToJSON.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ToJSON.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/ToJSON.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,28 @@ +/** +* 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 java.io.PrintWriter; + +/** + * A light-weight JSON rendering interface + */ +public interface ToJSON { + void toJSON(PrintWriter out); +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/View.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/View.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/View.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/View.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,211 @@ +/** +* 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 com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.servlet.RequestScoped; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.apache.hadoop.yarn.util.StringHelper.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for all views + */ +public abstract class View implements Params { + public static final Logger LOG = LoggerFactory.getLogger(View.class); + + @RequestScoped + public static class ViewContext { + final Controller.RequestContext rc; + int nestLevel = 0; + boolean wasInline; + + @Inject ViewContext(Controller.RequestContext ctx) { + rc = ctx; + } + + public int nestLevel() { return nestLevel; } + public boolean wasInline() { return wasInline; } + + public void set(int nestLevel, boolean wasInline) { + this.nestLevel = nestLevel; + this.wasInline = wasInline; + } + + public Controller.RequestContext requestContext() { return rc; } + } + + private ViewContext vc; + @Inject Injector injector; + + public View() { + // Makes injection in subclasses optional. + // Time will tell if this buy us more than the NPEs :) + } + + public View(ViewContext ctx) { + vc = ctx; + } + + /** + * The API to render the view + */ + public abstract void render(); + + public ViewContext context() { + if (vc == null) { + if (injector == null) { + // One downside of making the injection in subclasses optional + throw new WebAppException(join("Error accessing ViewContext from a\n", + "child constructor, either move the usage of the View methods\n", + "out of the constructor or inject the ViewContext into the\n", + "constructor")); + } + vc = injector.getInstance(ViewContext.class); + } + return vc; + } + + public Throwable error() { return context().rc.error; } + + public int status() { return context().rc.status; } + + public boolean inDevMode() { return context().rc.devMode; } + + public Injector injector() { return context().rc.injector; } + + public HttpServletRequest request() { + return context().rc.request; + } + + public HttpServletResponse response() { + return context().rc.response; + } + + public Map<String, String> moreParams() { + return context().rc.moreParams(); + } + + /** + * Get the cookies + * @return the cookies map + */ + public Map<String, Cookie> cookies() { + return context().rc.cookies(); + } + + public ServletOutputStream outputStream() { + try { + return response().getOutputStream(); + } catch (IOException e) { + throw new WebAppException(e); + } + } + + public PrintWriter writer() { + try { + return response().getWriter(); + } catch (IOException e) { + throw new WebAppException(e); + } + } + + /** + * Lookup a value from the current context. + * @param key to lookup + * @param defaultValue if key is missing + * @return the value of the key or the default value + */ + public String $(String key, String defaultValue) { + // moreParams take precedence + String value = moreParams().get(key); + if (value == null) { + value = request().getParameter(key); + } + return value == null ? defaultValue : value; + } + + /** + * Lookup a value from the current context + * @param key to lookup + * @return the value of the key or empty string + */ + public String $(String key) { + return $(key, ""); + } + + /** + * Set a context value. (e.g. UI properties for sub views.) + * Try to avoid any application (vs view/ui) logic. + * @param key to set + * @param value to set + */ + public void set(String key, String value) { + moreParams().put(key, value); + } + + public String prefix() { + return context().rc.prefix; + } + + public void setTitle(String title) { + set(TITLE, title); + } + + public void setTitle(String title, String url) { + setTitle(title); + set(TITLE_LINK, url); + } + + /** + * Create an url from url components + * @param parts components to join + * @return an url string + */ + public String url(String... parts) { + return ujoin(prefix(), parts); + } + + public ResponseInfo info(String about) { + return injector().getInstance(ResponseInfo.class).about(about); + } + + /** + * Render a sub-view + * @param cls the class of the sub-view + */ + public void render(Class<? extends SubView> cls) { + int saved = context().nestLevel; + injector().getInstance(cls).renderPartial(); + if (context().nestLevel != saved) { + throw new WebAppException("View "+ cls.getSimpleName() +" not complete"); + } + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApp.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApp.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApp.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApp.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,202 @@ +/** +* 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 com.google.common.base.CharMatcher; +import static com.google.common.base.Preconditions.*; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import com.google.inject.Provides; +import com.google.inject.servlet.GuiceFilter; +import com.google.inject.servlet.ServletModule; + +import java.util.List; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.http.HttpServer; +import org.apache.hadoop.yarn.util.StringHelper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @see WebApps for a usage example + */ +public abstract class WebApp extends ServletModule { + private static final Logger LOG = LoggerFactory.getLogger(WebApp.class); + + public enum HTTP { GET, POST, HEAD, PUT, DELETE }; + + private volatile String name; + private volatile Configuration conf; + private volatile HttpServer httpServer; + private volatile GuiceFilter guiceFilter; + private final Router router = new Router(); + + // index for the parsed route result + static final int R_PATH = 0; + static final int R_CONTROLLER = 1; + static final int R_ACTION = 2; + static final int R_PARAMS = 3; + + static final Splitter pathSplitter = + Splitter.on('/').trimResults().omitEmptyStrings(); + + void setHttpServer(HttpServer server) { + httpServer = checkNotNull(server, "http server"); + } + + @Provides public HttpServer httpServer() { return httpServer; } + + public int port() { + return checkNotNull(httpServer, "httpServer").getPort(); + } + + public void stop() { + try { + checkNotNull(httpServer, "httpServer").stop(); + checkNotNull(guiceFilter, "guiceFilter").destroy(); + } + catch (Exception e) { + throw new WebAppException(e); + } + } + + public void joinThread() { + try { + checkNotNull(httpServer, "httpServer").join(); + } catch (InterruptedException e) { + LOG.info("interrupted", e); + } + } + + void setConf(Configuration conf) { this.conf = conf; } + + @Provides public Configuration conf() { return conf; } + + @Provides Router router() { return router; } + + @Provides WebApp webApp() { return this; } + + void setName(String name) { this.name = name; } + + public String name() { return this.name; } + + void setHostClass(Class<?> cls) { + router.setHostClass(cls); + } + + void setGuiceFilter(GuiceFilter instance) { + guiceFilter = instance; + } + + @Override + public void configureServlets() { + setup(); + serve("/", "/__stop", StringHelper.join('/', name, '*')).with(Dispatcher.class); + } + + /** + * Setup of a webapp serving route. + * @param method the http method for the route + * @param pathSpec the path spec in the form of /controller/action/:args etc. + * @param cls the controller class + * @param action the controller method + */ + public void route(HTTP method, String pathSpec, + Class<? extends Controller> cls, String action) { + List<String> res = parseRoute(pathSpec); + router.add(method, res.get(R_PATH), cls, action, + res.subList(R_PARAMS, res.size())); + } + + public void route(String pathSpec, Class<? extends Controller> cls, + String action) { + route(HTTP.GET, pathSpec, cls, action); + } + + public void route(String pathSpec, Class<? extends Controller> cls) { + List<String> res = parseRoute(pathSpec); + router.add(HTTP.GET, res.get(R_PATH), cls, res.get(R_ACTION), + res.subList(R_PARAMS, res.size())); + } + + + /** + * /controller/action/:args => [/controller/action, controller, action, args] + * /controller/:args => [/controller, controller, index, args] + */ + static List<String> parseRoute(String pathSpec) { + List<String> result = Lists.newArrayList(); + result.add(getPrefix(checkNotNull(pathSpec, "pathSpec"))); + Iterable<String> parts = pathSplitter.split(pathSpec); + String controller = null, action = null; + for (String s : parts) { + if (controller == null) { + if (s.charAt(0) == ':') { + controller = "default"; + result.add(controller); + action = "index"; + result.add(action); + } else { + controller = s; + } + } else if (action == null) { + if (s.charAt(0) == ':') { + action = "index"; + result.add(action); + } else { + action = s; + } + } + result.add(s); + } + if (controller == null) { + result.add("default"); + } + if (action == null) { + result.add("index"); + } + return result; + } + + static String getPrefix(String pathSpec) { + int start = 0; + while (CharMatcher.WHITESPACE.matches(pathSpec.charAt(start))) { + ++start; + } + if (pathSpec.charAt(start) != '/') { + throw new WebAppException("Path spec syntax error: "+ pathSpec); + } + int ci = pathSpec.indexOf(':'); + if (ci == -1) { + ci = pathSpec.length(); + } + if (ci == 1) { + return "/"; + } + char c; + do { + c = pathSpec.charAt(--ci); + } while (c == '/' || CharMatcher.WHITESPACE.matches(c)); + return pathSpec.substring(start, ci + 1); + } + + public abstract void setup(); +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebAppException.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebAppException.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebAppException.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebAppException.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,38 @@ +/** +* 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 org.apache.hadoop.yarn.YarnException; + +public class WebAppException extends YarnException { + + private static final long serialVersionUID = 1L; + + public WebAppException(String msg) { + super(msg); + } + + public WebAppException(Throwable cause) { + super(cause); + } + + public WebAppException(String msg, Throwable cause) { + super(msg, cause); + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,222 @@ +/** +* 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.*; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.servlet.GuiceFilter; + +import java.net.ConnectException; +import java.net.URL; +import org.apache.commons.lang.StringUtils; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.http.HttpServer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helpers to create an embedded webapp. + * + * <h4>Quick start:</h4> + * <pre> + * WebApp wa = WebApps.$for(myApp).start();</pre> + * Starts a webapp with default routes binds to 0.0.0.0 (all network interfaces) + * on an ephemeral port, which can be obtained with:<pre> + * int port = wa.port();</pre> + * <h4>With more options:</h4> + * <pre> + * WebApp wa = WebApps.$for(myApp).at(address, port). + * with(configuration). + * start(new WebApp() { + * @Override public void setup() { + * route("/foo/action", FooController.class); + * route("/foo/:id", FooController.class, "show"); + * } + * });</pre> + */ +public class WebApps { + static final Logger LOG = LoggerFactory.getLogger(WebApps.class); + + public static class Builder<T> { + final String name; + final Class<T> api; + final T application; + String bindAddress = "0.0.0.0"; + int port = 0; + boolean findPort = false; + Configuration conf; + boolean devMode = false; + Module[] modules; + + Builder(String name, Class<T> api, T application) { + this.name = name; + this.api = api; + this.application = application; + } + + public Builder<T> at(String bindAddress) { + String[] parts = StringUtils.split(bindAddress, ':'); + if (parts.length == 2) { + return at(parts[0], Integer.parseInt(parts[1]), true); + } + return at(bindAddress, 0, true); + } + + public Builder<T> at(int port) { + return at("0.0.0.0", port, false); + } + + public Builder<T> at(String address, int port, boolean findPort) { + this.bindAddress = checkNotNull(address, "bind address"); + this.port = port; + this.findPort = findPort; + return this; + } + + public Builder<T> with(Configuration conf) { + this.conf = conf; + return this; + } + + public Builder<T> with(Module... modules) { + this.modules = modules; // OK + return this; + } + + public Builder<T> inDevMode() { + devMode = true; + return this; + } + + public WebApp start(WebApp webapp) { + if (webapp == null) { + webapp = new WebApp() { + @Override + public void setup() { + // Defaults should be fine in usual cases + } + }; + } + webapp.setName(name); + if (conf == null) { + conf = new Configuration(); + } + try { + if (application != null) { + webapp.setHostClass(application.getClass()); + } else { + String cls = inferHostClass(); + LOG.debug("setting webapp host class to {}", cls); + webapp.setHostClass(Class.forName(cls)); + } + if (devMode) { + if (port > 0) { + try { + new URL("http://localhost:"+ port +"/__stop").getContent(); + LOG.info("stopping existing webapp instance"); + Thread.sleep(100); + } catch (ConnectException e) { + LOG.info("no existing webapp instance found: {}", e.toString()); + } catch (Exception e) { + // should not be fatal + LOG.warn("error stopping existing instance: {}", e.toString()); + } + } else { + LOG.error("dev mode does NOT work with ephemeral port!"); + System.exit(1); + } + } + HttpServer server = + new HttpServer(name, bindAddress, port, findPort, conf); + server.addGlobalFilter("guice", GuiceFilter.class.getName(), null); + webapp.setConf(conf); + webapp.setHttpServer(server); + server.start(); + LOG.info("Web app /"+ name +" started at "+ server.getPort()); + } catch (Exception e) { + throw new WebAppException("Error starting http server", e); + } + Injector injector = Guice.createInjector(webapp, new AbstractModule() { + @Override @SuppressWarnings("unchecked") + protected void configure() { + if (api != null) { + bind(api).toInstance(application); + } + } + }); + LOG.info("Registered webapp guice modules"); + // save a guice filter instance for webapp stop (mostly for unit tests) + webapp.setGuiceFilter(injector.getInstance(GuiceFilter.class)); + if (devMode) { + injector.getInstance(Dispatcher.class).setDevMode(devMode); + LOG.info("in dev mode!"); + } + return webapp; + } + + public WebApp start() { + return start(null); + } + + private String inferHostClass() { + String thisClass = this.getClass().getName(); + Throwable t = new Throwable(); + for (StackTraceElement e : t.getStackTrace()) { + if (e.getClassName().equals(thisClass)) continue; + return e.getClassName(); + } + LOG.warn("could not infer host class from", t); + return thisClass; + } + } + + /** + * Create a new webapp builder. + * @see WebApps for a complete example + * @param <T> application (holding the embedded webapp) type + * @param prefix of the webapp + * @param api the api class for the application + * @param app the application instance + * @return a webapp builder + */ + public static <T> Builder<T> $for(String prefix, Class<T> api, T app) { + return new Builder<T>(prefix, api, app); + } + + // Short cut mostly for tests/demos + @SuppressWarnings("unchecked") + public static <T> Builder<T> $for(String prefix, T app) { + return $for(prefix, (Class<T>)app.getClass(), app); + } + + // Ditto + @SuppressWarnings("unchecked") + public static <T> Builder<T> $for(T app) { + return $for("", app); + } + + public static <T> Builder<T> $for(String prefix) { + return $for(prefix, null, null); + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/HelloWorld.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/HelloWorld.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/HelloWorld.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/HelloWorld.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,53 @@ +/** +* 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.example; + +import org.apache.hadoop.yarn.webapp.Controller; +import org.apache.hadoop.yarn.webapp.WebApps; +import org.apache.hadoop.yarn.webapp.view.HtmlPage; + +/** + * The obligatory example. No xml/jsp/templates/config files! No + * proliferation of strange annotations either :) + * + * <p>3 in 1 example. Check results at + * <br>http://localhost:8888/hello and + * <br>http://localhost:8888/hello/html + * <br>http://localhost:8888/hello/json + */ +public class HelloWorld { + public static class Hello extends Controller { + @Override public void index() { renderText("Hello world!"); } + public void html() { setTitle("Hello world!"); } + public void json() { renderJSON("Hello world!"); } + } + + public static class HelloView extends HtmlPage { + @Override protected void render(Page.HTML<_> html) { + html. // produces valid html 4.01 strict + title($("title")). + p("#hello-for-css"). + _($("title"))._()._(); + } + } + + public static void main(String[] args) { + WebApps.$for(new HelloWorld()).at(8888).inDevMode().start().joinThread(); + } +} Added: hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/MyApp.java URL: http://svn.apache.org/viewvc/hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/MyApp.java?rev=1082677&view=auto ============================================================================== --- hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/MyApp.java (added) +++ hadoop/mapreduce/branches/MR-279/yarn/yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/example/MyApp.java Thu Mar 17 20:21:13 2011 @@ -0,0 +1,75 @@ +/** +* 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.example; + +import com.google.inject.Inject; + +import org.apache.hadoop.yarn.webapp.Controller; +import org.apache.hadoop.yarn.webapp.WebApps; +import org.apache.hadoop.yarn.webapp.view.HtmlPage; + +/** + * The embedded UI serves two pages at: + * <br>http://localhost:8888/my and + * <br>http://localhost:8888/my/anythingYouWant + */ +public class MyApp { + + // This is an app API + public String anyAPI() { return "anything â, really!"; } + + // Note this is static so it can be in any files. + public static class MyController extends Controller { + final MyApp app; + + // The app injection is optional + @Inject MyController(MyApp app, RequestContext ctx) { + super(ctx); + this.app = app; + } + + @Override + public void index() { + set("anything", "something â¯"); + } + + public void anythingYouWant() { + set("anything", app.anyAPI()); + } + } + + // Ditto + public static class MyView extends HtmlPage { + // You can inject the app in views if needed. + @Override + public void render(Page.HTML<_> html) { + html. + title("My App"). + p("#content_id_for_css_styling"). + _("You can have", $("anything"))._()._(); + // Note, there is no _(); (to parent element) method at root level. + // and IDE provides instant feedback on what level you're on in + // the auto-completion drop-downs. + } + } + + public static void main(String[] args) throws Exception { + WebApps.$for(new MyApp()).at(8888).inDevMode().start().joinThread(); + } +}
