/** * 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.hamlet; import com.google.common.base.Joiner; import static com.google.common.base.Preconditions.*; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import java.io.PrintWriter; import java.util.EnumSet; import static java.util.EnumSet.*; import java.util.Iterator; import static org.apache.commons.lang.StringEscapeUtils.*; import static org.apache.hadoop.yarn.webapp.hamlet.HamletImpl.EOpt.*; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.yarn.webapp.SubView; import org.apache.hadoop.yarn.webapp.WebAppException; /** * A simple unbuffered generic hamlet implementation. * * Zero copy but allocation on every element, which could be * optimized to use a thread-local element pool. * * Prints HTML as it builds. So the order is important. */ @InterfaceAudience.Private public class HamletImpl extends HamletSpec { private static final String INDENT_CHARS = " "; private static final Splitter SS = Splitter.on('.'). omitEmptyStrings().trimResults(); private static final Joiner SJ = Joiner.on(' '); private static final Joiner CJ = Joiner.on(", "); static final int S_ID = 0; static final int S_CLASS = 1; int nestLevel; int indents; // number of indent() called. mostly for testing. private final PrintWriter out; private final StringBuilder sb = new StringBuilder(); // not shared private boolean wasInline = false; /** * Element options. (whether it needs end tag, is inline etc.) */ public enum EOpt { /** needs end(close) tag */ ENDTAG, /** The content is inline */ INLINE, /** The content is preformatted */ PRE }; /** * The base class for elements * @param <T> type of the parent (containing) element for the element */ public class EImp<T extends _> implements _Child { private final String name; private final T parent; // short cut for parent element private final EnumSet<EOpt> opts; // element options private boolean started = false; private boolean attrsClosed = false; EImp(String name, T parent, EnumSet<EOpt> opts) { this.name = name; this.parent = parent; this.opts = opts; } @Override public T _() { closeAttrs(); --nestLevel; printEndTag(name, opts); return parent; } protected void _p(boolean quote, Object... args) { closeAttrs(); for (Object s : args) { if (!opts.contains(PRE)) { indent(opts); } out.print(quote ? escapeHtml(String.valueOf(s)) : String.valueOf(s)); if (!opts.contains(INLINE) && !opts.contains(PRE)) { out.println(); } } } protected void _v(Class<? extends SubView> cls) { closeAttrs(); subView(cls); } protected void closeAttrs() { if (!attrsClosed) { startIfNeeded(); ++nestLevel; out.print('>'); if (!opts.contains(INLINE) && !opts.contains(PRE)) { out.println(); } attrsClosed = true; } } protected void addAttr(String name, String value) { checkState(!attrsClosed, "attribute added after content"); startIfNeeded(); printAttr(name, value); } protected void addAttr(String name, Object value) { addAttr(name, String.valueOf(value)); } protected void addMediaAttr(String name, EnumSet<Media> media) { // 6.13 comma-separated list addAttr(name, CJ.join(media)); } protected void addRelAttr(String name, EnumSet<LinkType> types) { // 6.12 space-separated list addAttr(name, SJ.join(types)); } private void startIfNeeded() { if (!started) { printStartTag(name, opts); started = true; } } protected void _inline(boolean choice) { if (choice) { opts.add(INLINE); } else { opts.remove(INLINE); } } protected void _endTag(boolean choice) { if (choice) { opts.add(ENDTAG); } else { opts.remove(ENDTAG); } } protected void _pre(boolean choice) { if (choice) { opts.add(PRE); } else { opts.remove(PRE); } } } public class Generic<T extends _> extends EImp<T> implements PCData { Generic(String name, T parent, EnumSet<EOpt> opts) { super(name, parent, opts); } public Generic<T> _inline() { super._inline(true); return this; } public Generic<T> _noEndTag() { super._endTag(false); return this; } public Generic<T> _pre() { super._pre(true); return this; } public Generic<T> _attr(String name, String value) { addAttr(name, value); return this; } public Generic<Generic<T>> _elem(String name, EnumSet<EOpt> opts) { closeAttrs(); return new Generic<Generic<T>>(name, this, opts); } public Generic<Generic<T>> elem(String name) { return _elem(name, of(ENDTAG)); } @Override public Generic<T> _(Object... lines) { _p(true, lines); return this; } @Override public Generic<T> _r(Object... lines) { _p(false, lines); return this; } } public HamletImpl(PrintWriter out, int nestLevel, boolean wasInline) { this.out = out; this.nestLevel = nestLevel; this.wasInline = wasInline; } public int nestLevel() { return nestLevel; } public boolean wasInline() { return wasInline; } public void setWasInline(boolean state) { wasInline = state; } public PrintWriter getWriter() { return out; } /** * Create a root-level generic element. * Mostly for testing purpose. * @param <T> type of the parent element * @param name of the element * @param opts {@link EOpt element options} * @return the element */ public <T extends _> Generic<T> root(String name, EnumSet<EOpt> opts) { return new Generic<T>(name, null, opts); } public <T extends _> Generic<T> root(String name) { return root(name, of(ENDTAG)); } protected void printStartTag(String name, EnumSet<EOpt> opts) { indent(opts); sb.setLength(0); out.print(sb.append('<').append(name).toString()); // for easier mock test } protected void indent(EnumSet<EOpt> opts) { if (opts.contains(INLINE) && wasInline) { return; } if (wasInline) { out.println(); } wasInline = opts.contains(INLINE) || opts.contains(PRE); for (int i = 0; i < nestLevel; ++i) { out.print(INDENT_CHARS); } ++indents; } protected void printEndTag(String name, EnumSet<EOpt> opts) { if (!opts.contains(ENDTAG)) { return; } if (!opts.contains(PRE)) { indent(opts); } else { wasInline = opts.contains(INLINE); } sb.setLength(0); out.print(sb.append("</").append(name).append('>').toString()); // ditto if (!opts.contains(INLINE)) { out.println(); } } protected void printAttr(String name, String value) { sb.setLength(0); sb.append(' ').append(name); if (value != null) { sb.append("=\"").append(escapeHtml(value)).append("\""); } out.print(sb.toString()); } /** * Sub-classes should override this to do something interesting. * @param cls the sub-view class */ protected void subView(Class<? extends SubView> cls) { indent(of(ENDTAG)); // not an inline view sb.setLength(0); out.print(sb.append('[').append(cls.getName()).append(']').toString()); out.println(); } /** * Parse selector into id and classes * @param selector in the form of (#id)?(.class)* * @return an two element array [id, "space-separated classes"]. * Either element could be null. * @throws WebAppException when both are null or syntax error. */ public static String[] parseSelector(String selector) { String[] result = new String[]{null, null}; Iterable<String> rs = SS.split(selector); Iterator<String> it = rs.iterator(); if (it.hasNext()) { String maybeId = it.next(); if (maybeId.charAt(0) == '#') { result[S_ID] = maybeId.substring(1); if (it.hasNext()) { result[S_CLASS] = SJ.join(Iterables.skip(rs, 1)); } } else { result[S_CLASS] = SJ.join(rs); } return result; } throw new WebAppException("Error parsing selector: "+ selector); } /** * Set id and/or class attributes for an element. * @param <E> type of the element * @param e the element * @param selector Haml form of "(#id)?(.class)*" * @return the element */ public static <E extends CoreAttrs> E setSelector(E e, String selector) { String[] res = parseSelector(selector); if (res[S_ID] != null) { e.$id(res[S_ID]); } if (res[S_CLASS] != null) { e.$class(res[S_CLASS]); } return e; } public static <E extends LINK> E setLinkHref(E e, String href) { if (href.endsWith(".css")) { e.$rel("stylesheet"); // required in html5 } e.$href(href); return e; } public static <E extends SCRIPT> E setScriptSrc(E e, String src) { if (src.endsWith(".js")) { e.$type("text/javascript"); // required in html4 } e.$src(src); return e; } }