You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2016/08/01 17:29:57 UTC
[08/53] [partial] incubator-juneau git commit: Merge changes from
GitHub repo.
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParser.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParser.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParser.java
new file mode 100644
index 0000000..bb1951d
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParser.java
@@ -0,0 +1,732 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import static javax.xml.stream.XMLStreamConstants.*;
+import static org.apache.juneau.html.HtmlParser.Tag.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import javax.xml.namespace.*;
+import javax.xml.stream.*;
+import javax.xml.stream.events.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.transform.*;
+
+/**
+ * Parses text generated by the {@link HtmlSerializer} class back into a POJO model.
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ * Handles <code>Content-Type</code> types: <code>text/html</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ * See the {@link HtmlSerializer} class for a description of the HTML generated.
+ * <p>
+ * This class is used primarily for automated testing of the {@link HtmlSerializer} class.
+ *
+ *
+ * <h6 class='topic'>Configurable properties</h6>
+ * <p>
+ * This class has the following properties associated with it:
+ * <ul>
+ * <li>{@link HtmlSerializerContext}
+ * </ul>
+ *
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+@Consumes({"text/html","text/html+stripped"})
+public final class HtmlParser extends ReaderParser {
+
+ /** Default parser, all default settings.*/
+ public static final HtmlParser DEFAULT = new HtmlParser().lock();
+
+ /*
+ * Reads anything starting at the current event.
+ * <p>
+ * Precondition: Must be pointing at START_ELEMENT or CHARACTERS event.
+ * Postcondition: Pointing at next event to be processed.
+ */
+ private <T> T parseAnything(HtmlParserSession session, ClassMeta<T> nt, XMLEventReader r, Object outer) throws Exception {
+
+ BeanContext bc = session.getBeanContext();
+ if (nt == null)
+ nt = (ClassMeta<T>)object();
+ PojoTransform<T,Object> transform = (PojoTransform<T,Object>)nt.getPojoTransform();
+ ClassMeta<?> ft = nt.getTransformedClassMeta();
+ session.setCurrentClass(ft);
+
+ Object o = null;
+
+ XMLEvent event = r.nextEvent();
+ while (! (event.isStartElement() || (event.isCharacters() && ! event.asCharacters().isWhiteSpace()) || event.isEndDocument()))
+ event = r.nextEvent();
+
+ if (event.isEndDocument())
+ throw new XMLStreamException("Unexpected end of stream in parseAnything for type '"+nt+"'", event.getLocation());
+
+ if (event.isCharacters()) {
+ String text = parseCharacters(event, r);
+ if (ft.isObject())
+ o = text;
+ else if (ft.isCharSequence())
+ o = text;
+ else if (ft.isNumber())
+ o = parseNumber(text, (Class<? extends Number>)nt.getInnerClass());
+ else if (ft.isChar())
+ o = text.charAt(0);
+ else if (ft.isBoolean())
+ o = Boolean.parseBoolean(text);
+ else if (ft.canCreateNewInstanceFromString(outer))
+ o = ft.newInstanceFromString(outer, text);
+ else if (ft.canCreateNewInstanceFromNumber(outer))
+ o = ft.newInstanceFromNumber(outer, parseNumber(text, ft.getNewInstanceFromNumberClass()));
+ else
+ throw new XMLStreamException("Unexpected characters '"+event.asCharacters().getData()+"' for type '"+nt+"'", event.getLocation());
+
+ } else {
+ Tag tag = Tag.forString(event.asStartElement().getName().getLocalPart(), false);
+ String tableType = "object";
+ String text = "";
+
+ if (tag.isOneOf(STRING, NUMBER, BOOLEAN, BR, FF, BS, TB))
+ text = parseCharacters(event, r);
+
+ if (tag == TABLE) {
+ Map<String,String> attrs = getAttributes(event);
+ tableType = attrs.get("type");
+ String c = attrs.get("_class");
+ if (c != null)
+ ft = nt = (ClassMeta<T>)bc.getClassMetaFromString(c);
+ }
+
+ boolean isValid = true;
+
+ if (tag == NULL)
+ nextTag(r, xNULL);
+ else if (tag == A)
+ o = parseAnchor(session, event, r, nt);
+ else if (ft.isObject()) {
+ if (tag == STRING)
+ o = text;
+ else if (tag == NUMBER)
+ o = parseNumber(text, null);
+ else if (tag == BOOLEAN)
+ o = Boolean.parseBoolean(text);
+ else if (tag == TABLE) {
+ if (tableType.equals("object")) {
+ o = parseIntoMap(session, r, (Map)new ObjectMap(bc), ft.getKeyType(), ft.getValueType());
+ } else if (tableType.equals("array")) {
+ o = parseTableIntoCollection(session, r, (Collection)new ObjectList(bc), ft.getElementType());
+ } else
+ isValid = false;
+ }
+ else if (tag == UL)
+ o = parseIntoCollection(session, r, new ObjectList(bc), null);
+ }
+ else if (tag == STRING && ft.isCharSequence())
+ o = text;
+ else if (tag == STRING && ft.isChar())
+ o = text.charAt(0);
+ else if (tag == STRING && ft.canCreateNewInstanceFromString(outer))
+ o = ft.newInstanceFromString(outer, text);
+ else if (tag == NUMBER && ft.isNumber())
+ o = parseNumber(text, (Class<? extends Number>)ft.getInnerClass());
+ else if (tag == NUMBER && ft.canCreateNewInstanceFromNumber(outer))
+ o = ft.newInstanceFromNumber(outer, parseNumber(text, ft.getNewInstanceFromNumberClass()));
+ else if (tag == BOOLEAN && ft.isBoolean())
+ o = Boolean.parseBoolean(text);
+ else if (tag == TABLE) {
+ if (tableType.equals("object")) {
+ if (ft.isMap()) {
+ o = parseIntoMap(session, r, (Map)(ft.canCreateNewInstance(outer) ? ft.newInstance(outer) : new ObjectMap(bc)), ft.getKeyType(), ft.getValueType());
+ } else if (ft.canCreateNewInstanceFromObjectMap(outer)) {
+ ObjectMap m = new ObjectMap(bc);
+ parseIntoMap(session, r, m, string(), object());
+ o = ft.newInstanceFromObjectMap(outer, m);
+ } else if (ft.canCreateNewBean(outer)) {
+ BeanMap m = bc.newBeanMap(outer, ft.getInnerClass());
+ o = parseIntoBean(session, r, m).getBean();
+ }
+ else
+ isValid = false;
+ } else if (tableType.equals("array")) {
+ if (ft.isCollection())
+ o = parseTableIntoCollection(session, r, (Collection)(ft.canCreateNewInstance(outer) ? ft.newInstance(outer) : new ObjectList(bc)), ft.getElementType());
+ else if (ft.isArray())
+ o = bc.toArray(ft, parseTableIntoCollection(session, r, new ArrayList(), ft.getElementType()));
+ else
+ isValid = false;
+ } else
+ isValid = false;
+ } else if (tag == UL) {
+ if (ft.isCollection())
+ o = parseIntoCollection(session, r, (Collection)(ft.canCreateNewInstance(outer) ? ft.newInstance(outer) : new ObjectList(bc)), ft.getElementType());
+ else if (ft.isArray())
+ o = bc.toArray(ft, parseIntoCollection(session, r, new ArrayList(), ft.getElementType()));
+ else
+ isValid = false;
+ } else
+ isValid = false;
+
+ if (! isValid)
+ throw new XMLStreamException("Unexpected tag '"+tag+"' for type '"+nt+"'", event.getLocation());
+ }
+
+
+ if (transform != null && o != null)
+ o = transform.normalize(o, nt);
+
+ if (outer != null)
+ setParent(nt, o, outer);
+
+ return (T)o;
+ }
+
+ /*
+ * Reads an anchor tag and converts it into a bean.
+ */
+ private <T> T parseAnchor(HtmlParserSession session, XMLEvent e, XMLEventReader r, ClassMeta<T> beanType) throws XMLStreamException {
+ BeanContext bc = session.getBeanContext();
+ String href = e.asStartElement().getAttributeByName(new QName("href")).getValue();
+ String name = parseCharacters(e, r);
+ Class<T> beanClass = beanType.getInnerClass();
+ if (beanClass.isAnnotationPresent(HtmlLink.class)) {
+ HtmlLink h = beanClass.getAnnotation(HtmlLink.class);
+ BeanMap<T> m = bc.newBeanMap(beanClass);
+ m.put(h.hrefProperty(), href);
+ m.put(h.nameProperty(), name);
+ return m.getBean();
+ }
+ return bc.convertToType(href, beanType);
+ }
+
+ private Map<String,String> getAttributes(XMLEvent e) {
+ Map<String,String> m = new TreeMap<String,String>() ;
+ for (Iterator i = e.asStartElement().getAttributes(); i.hasNext();) {
+ Attribute a = (Attribute)i.next();
+ m.put(a.getName().getLocalPart(), a.getValue());
+ }
+ return m;
+ }
+
+ /*
+ * Reads contents of <table> element.
+ * Precondition: Must be pointing at <table> event.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private <K,V> Map<K,V> parseIntoMap(HtmlParserSession session, XMLEventReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType) throws Exception {
+ Tag tag = nextTag(r, TR);
+
+ // Skip over the column headers.
+ nextTag(r, TH);
+ parseElementText(r, xTH);
+ nextTag(r, TH);
+ parseElementText(r, xTH);
+ nextTag(r, xTR);
+
+ while (true) {
+ tag = nextTag(r, TR, xTABLE);
+ if (tag == xTABLE)
+ break;
+ nextTag(r, TD);
+ K key = parseAnything(session, keyType, r, m);
+ nextTag(r, xTD);
+ nextTag(r, TD);
+ V value = parseAnything(session, valueType, r, m);
+ setName(valueType, value, key);
+ m.put(key, value);
+ nextTag(r, xTD);
+ nextTag(r, xTR);
+ }
+
+ return m;
+ }
+
+ /*
+ * Reads contents of <ul> element.
+ * Precondition: Must be pointing at event following <ul> event.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private <E> Collection<E> parseIntoCollection(HtmlParserSession session, XMLEventReader r, Collection<E> l, ClassMeta<E> elementType) throws Exception {
+ while (true) {
+ Tag tag = nextTag(r, LI, xUL);
+ if (tag == xUL)
+ break;
+ l.add(parseAnything(session, elementType, r, l));
+ nextTag(r, xLI);
+ }
+ return l;
+ }
+
+ /*
+ * Reads contents of <ul> element into an Object array.
+ * Precondition: Must be pointing at event following <ul> event.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private Object[] parseArgs(HtmlParserSession session, XMLEventReader r, ClassMeta<?>[] argTypes) throws Exception {
+ Object[] o = new Object[argTypes.length];
+ int i = 0;
+ while (true) {
+ Tag tag = nextTag(r, LI, xUL);
+ if (tag == xUL)
+ break;
+ o[i] = parseAnything(session, argTypes[i], r, session.getOuter());
+ i++;
+ nextTag(r, xLI);
+ }
+ return o;
+ }
+
+ /*
+ * Reads contents of <ul> element.
+ * Precondition: Must be pointing at event following <ul> event.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private <E> Collection<E> parseTableIntoCollection(HtmlParserSession session, XMLEventReader r, Collection<E> l, ClassMeta<E> elementType) throws Exception {
+
+ BeanContext bc = session.getBeanContext();
+ if (elementType == null)
+ elementType = (ClassMeta<E>)object();
+
+ Tag tag = nextTag(r, TR);
+ List<String> keys = new ArrayList<String>();
+ while (true) {
+ tag = nextTag(r, TH, xTR);
+ if (tag == xTR)
+ break;
+ keys.add(parseElementText(r, xTH));
+ }
+
+ while (true) {
+ XMLEvent event = r.nextTag();
+ tag = Tag.forEvent(event);
+ if (tag == xTABLE)
+ break;
+ if (elementType.canCreateNewBean(l)) {
+ BeanMap m = bc.newBeanMap(l, elementType.getInnerClass());
+ for (int i = 0; i < keys.size(); i++) {
+ tag = nextTag(r, TD, NULL);
+ if (tag == NULL) {
+ m = null;
+ nextTag(r, xNULL);
+ break;
+ }
+ String key = keys.get(i);
+ BeanMapEntry e = m.getProperty(key);
+ if (e == null) {
+ //onUnknownProperty(key, m, -1, -1);
+ parseAnything(session, object(), r, l);
+ } else {
+ BeanPropertyMeta<?> bpm = e.getMeta();
+ ClassMeta<?> cm = bpm.getClassMeta();
+ Object value = parseAnything(session, cm, r, m.getBean(false));
+ setName(cm, value, key);
+ bpm.set(m, value);
+ }
+ nextTag(r, xTD);
+ }
+ l.add(m == null ? null : (E)m.getBean());
+ } else {
+ String c = getAttributes(event).get("_class");
+ Map m = (Map)(elementType.isMap() && elementType.canCreateNewInstance(l) ? elementType.newInstance(l) : new ObjectMap(bc));
+ for (int i = 0; i < keys.size(); i++) {
+ tag = nextTag(r, TD, NULL);
+ if (tag == NULL) {
+ m = null;
+ nextTag(r, xNULL);
+ break;
+ }
+ String key = keys.get(i);
+ if (m != null) {
+ ClassMeta<?> et = elementType.getElementType();
+ Object value = parseAnything(session, et, r, l);
+ setName(et, value, key);
+ m.put(key, value);
+ }
+ nextTag(r, xTD);
+ }
+ if (m != null && c != null) {
+ ObjectMap m2 = (m instanceof ObjectMap ? (ObjectMap)m : new ObjectMap(m).setBeanContext(session.getBeanContext()));
+ m2.put("_class", c);
+ l.add((E)m2.cast());
+ } else {
+ l.add((E)m);
+ }
+ }
+ nextTag(r, xTR);
+ }
+ return l;
+ }
+
+ /*
+ * Reads contents of <table> element.
+ * Precondition: Must be pointing at event following <table> event.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private <T> BeanMap<T> parseIntoBean(HtmlParserSession session, XMLEventReader r, BeanMap<T> m) throws Exception {
+ Tag tag = nextTag(r, TR);
+
+ // Skip over the column headers.
+ nextTag(r, TH);
+ parseElementText(r, xTH);
+ nextTag(r, TH);
+ parseElementText(r, xTH);
+ nextTag(r, xTR);
+
+ while (true) {
+ tag = nextTag(r, TR, xTABLE);
+ if (tag == xTABLE)
+ break;
+ nextTag(r, TD);
+ String key = parseElementText(r, xTD);
+ nextTag(r, TD);
+ BeanPropertyMeta pMeta = m.getPropertyMeta(key);
+ if (pMeta == null) {
+ if (m.getMeta().isSubTyped()) {
+ Object value = parseAnything(session, object(), r, m.getBean(false));
+ m.put(key, value);
+ } else {
+ onUnknownProperty(session, key, m, -1, -1);
+ parseAnything(session, object(), r, null);
+ }
+ } else {
+ ClassMeta<?> cm = pMeta.getClassMeta();
+ Object value = parseAnything(session, cm, r, m.getBean(false));
+ setName(cm, value, key);
+ pMeta.set(m, value);
+ }
+ nextTag(r, xTD);
+ nextTag(r, xTR);
+ }
+ return m;
+ }
+
+ /*
+ * Parse until the next event is an end tag.
+ */
+ private String parseCharacters(XMLEvent e, XMLEventReader r) throws XMLStreamException {
+
+ List<String> strings = new LinkedList<String>();
+
+ while (true) {
+ int eventType = e.getEventType();
+ if (eventType == CHARACTERS) {
+ Characters c = e.asCharacters();
+ if (! c.isWhiteSpace())
+ strings.add(c.getData());
+ }
+ else if (eventType == START_ELEMENT) {
+ Tag tag = Tag.forEvent(e);
+ if (tag == BR)
+ strings.add("\n");
+ else if (tag == FF)
+ strings.add("\f");
+ else if (tag == BS)
+ strings.add("\b");
+ else if (tag == TB)
+ strings.add("\t");
+ }
+ // Ignore all other elements.
+
+ XMLEvent eNext = r.peek();
+
+ if (eNext.isStartElement() || eNext.isEndElement()) {
+ Tag tag = Tag.forEvent(eNext);
+ if (! (tag.isOneOf(A, xA, BR, xBR, FF, xFF, BS, xBS, TB, xTB, STRING, xSTRING, NUMBER, xNUMBER, BOOLEAN, xBOOLEAN)))
+ return trim(join(strings));
+ } else if (eNext.isEndDocument()) {
+ return trim(join(strings));
+ }
+
+ e = r.nextEvent();
+ }
+ }
+
+ private String trim(String s) {
+ int i2 = 0, i3;
+ for (i2 = 0; i2 < s.length(); i2++) {
+ char c = s.charAt(i2);
+ if (c != ' ')
+ break;
+ }
+ for (i3 = s.length(); i3 > i2; i3--) {
+ char c = s.charAt(i3-1);
+ if (c != ' ')
+ break;
+ }
+ return s.substring(i2, i3);
+ }
+
+ /*
+ * Reads the element text of the current element, accounting for <a> and <br> tags. <br>
+ * Precondition: Must be pointing at first event AFTER the start tag.
+ * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event.
+ */
+ private String parseElementText(XMLEventReader r, Tag endTag) throws XMLStreamException {
+
+ List<String> strings = new LinkedList<String>();
+
+ XMLEvent e = r.nextEvent();
+ Tag nTag = (e.isEndElement() ? Tag.forEvent(e) : null);
+
+ while (nTag != endTag) {
+ if (e.isCharacters())
+ strings.add(parseCharacters(e, r));
+ e = r.nextEvent();
+
+ if (e.getEventType() == END_ELEMENT)
+ nTag = Tag.forEvent(e);
+
+ if (nTag == endTag)
+ return join(strings);
+ }
+
+ return "";
+ }
+
+ enum Tag {
+
+ TABLE(1,"<table>"),
+ TR(2,"<tr>"),
+ TH(3,"<th>"),
+ TD(4,"<td>"),
+ UL(5,"<ul>"),
+ LI(6,"<li>"),
+ STRING(7,"<string>"),
+ NUMBER(8,"<number>"),
+ BOOLEAN(9,"<boolean>"),
+ NULL(10,"<null>"),
+ A(11,"<a>"),
+ BR(12,"<br>"), // newline
+ FF(13,"<ff>"), // formfeed
+ BS(14,"<bs>"), // backspace
+ TB(15,"<tb>"), // tab
+ xTABLE(-1,"</table>"),
+ xTR(-2,"</tr>"),
+ xTH(-3,"</th>"),
+ xTD(-4,"</td>"),
+ xUL(-5,"</ul>"),
+ xLI(-6,"</li>"),
+ xSTRING(-7,"</string>"),
+ xNUMBER(-8,"</number>"),
+ xBOOLEAN(-9,"</boolean>"),
+ xNULL(-10,"</null>"),
+ xA(-11,"</a>"),
+ xBR(-12,"</br>"),
+ xFF(-13,"</ff>"),
+ xBS(-14,"</bs>"),
+ xTB(-15,"</tb>");
+
+ private Map<Integer,Tag> cache = new HashMap<Integer,Tag>();
+
+ int id;
+ String label;
+
+ Tag(int id, String label) {
+ this.id = id;
+ this.label = label;
+ cache.put(id, this);
+ }
+
+ static Tag forEvent(XMLEvent event) throws XMLStreamException {
+ if (event.isStartElement())
+ return forString(event.asStartElement().getName().getLocalPart(), false);
+ else if (event.isEndElement())
+ return forString(event.asEndElement().getName().getLocalPart(), true);
+ throw new XMLStreamException("Invalid call to Tag.forEvent on event of type ["+event.getEventType()+"]");
+ }
+
+ private static Tag forString(String tag, boolean end) throws XMLStreamException {
+ char c = tag.charAt(0);
+ Tag t = null;
+ if (c == 'u')
+ t = (end ? xUL : UL);
+ else if (c == 'l')
+ t = (end ? xLI : LI);
+ else if (c == 's')
+ t = (end ? xSTRING : STRING);
+ else if (c == 'b') {
+ c = tag.charAt(1);
+ if (c == 'o')
+ t = (end ? xBOOLEAN : BOOLEAN);
+ else if (c == 'r')
+ t = (end ? xBR : BR);
+ else if (c == 's')
+ t = (end ? xBS : BS);
+ }
+ else if (c == 'a')
+ t = (end ? xA : A);
+ else if (c == 'n') {
+ c = tag.charAt(2);
+ if (c == 'm')
+ t = (end ? xNUMBER : NUMBER);
+ else if (c == 'l')
+ t = (end ? xNULL : NULL);
+ }
+ else if (c == 't') {
+ c = tag.charAt(1);
+ if (c == 'a')
+ t = (end ? xTABLE : TABLE);
+ else if (c == 'r')
+ t = (end ? xTR : TR);
+ else if (c == 'h')
+ t = (end ? xTH : TH);
+ else if (c == 'd')
+ t = (end ? xTD : TD);
+ else if (c == 'b')
+ t = (end ? xTB : TB);
+ }
+ else if (c == 'f')
+ t = (end ? xFF : FF);
+ if (t == null)
+ throw new XMLStreamException("Unknown tag '"+tag+"' encountered");
+ return t;
+ }
+
+ @Override /* Object */
+ public String toString() {
+ return label;
+ }
+
+ public boolean isOneOf(Tag...tags) {
+ for (Tag tag : tags)
+ if (tag == this)
+ return true;
+ return false;
+ }
+ }
+
+ /*
+ * Reads the current tag. Advances past anything that's not a start or end tag. Throws an exception if
+ * it's not one of the expected tags.
+ * Precondition: Must be pointing before the event we want to parse.
+ * Postcondition: Pointing at the tag just parsed.
+ */
+ private Tag nextTag(XMLEventReader r, Tag...expected) throws XMLStreamException {
+ XMLEvent event = r.nextTag();
+ Tag tag = Tag.forEvent(event);
+ if (expected.length == 0)
+ return tag;
+ for (Tag t : expected)
+ if (t == tag)
+ return tag;
+ throw new XMLStreamException("Unexpected tag: " + tag, event.getLocation());
+ }
+
+ private String join(List<String> s) {
+ if (s.size() == 0)
+ return "";
+ if (s.size() == 1)
+ return s.get(0);
+ StringBuilder sb = new StringBuilder();
+ for (String ss : s)
+ sb.append(ss);
+ return sb.toString();
+ }
+
+ //--------------------------------------------------------------------------------
+ // Overridden methods
+ //--------------------------------------------------------------------------------
+
+ @Override /* Parser */
+ public HtmlParserSession createSession(Object input, ObjectMap properties, Method javaMethod, Object outer) {
+ return new HtmlParserSession(getContext(HtmlParserContext.class), getBeanContext(), input, properties, javaMethod, outer);
+ }
+
+ @Override /* Parser */
+ protected <T> T doParse(ParserSession session, ClassMeta<T> type) throws Exception {
+ type = session.getBeanContext().normalizeClassMeta(type);
+ HtmlParserSession s = (HtmlParserSession)session;
+ return parseAnything(s, type, s.getXmlEventReader(), session.getOuter());
+ }
+
+ @Override /* ReaderParser */
+ protected <K,V> Map<K,V> doParseIntoMap(ParserSession session, Map<K,V> m, Type keyType, Type valueType) throws Exception {
+ HtmlParserSession s = (HtmlParserSession)session;
+ return parseIntoMap(s, s.getXmlEventReader(), m, s.getBeanContext().getClassMeta(keyType), s.getBeanContext().getClassMeta(valueType));
+ }
+
+ @Override /* ReaderParser */
+ protected <E> Collection<E> doParseIntoCollection(ParserSession session, Collection<E> c, Type elementType) throws Exception {
+ HtmlParserSession s = (HtmlParserSession)session;
+ return parseIntoCollection(s, s.getXmlEventReader(), c, s.getBeanContext().getClassMeta(elementType));
+ }
+
+ @Override /* ReaderParser */
+ protected Object[] doParseArgs(ParserSession session, ClassMeta<?>[] argTypes) throws Exception {
+ HtmlParserSession s = (HtmlParserSession)session;
+ return parseArgs(s, s.getXmlEventReader(), argTypes);
+ }
+
+ @Override /* CoreApi */
+ public HtmlParser setProperty(String property, Object value) throws LockedException {
+ super.setProperty(property, value);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlParser setProperties(ObjectMap properties) throws LockedException {
+ super.setProperties(properties);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlParser addNotBeanClasses(Class<?>...classes) throws LockedException {
+ super.addNotBeanClasses(classes);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlParser addTransforms(Class<?>...classes) throws LockedException {
+ super.addTransforms(classes);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public <T> HtmlParser addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException {
+ super.addImplClass(interfaceClass, implClass);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlParser setClassLoader(ClassLoader classLoader) throws LockedException {
+ super.setClassLoader(classLoader);
+ return this;
+ }
+
+ @Override /* Lockable */
+ public HtmlParser lock() {
+ super.lock();
+ return this;
+ }
+
+ @Override /* Lockable */
+ public HtmlParser clone() {
+ try {
+ return (HtmlParser)super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e); // Shouldn't happen
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserContext.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserContext.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserContext.java
new file mode 100644
index 0000000..716cc0a
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserContext.java
@@ -0,0 +1,62 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Configurable properties on the {@link HtmlParser} class.
+ * <p>
+ * Context properties are set by calling {@link ContextFactory#setProperty(String, Object)} on the context factory
+ * returned {@link CoreApi#getContextFactory()}.
+ * <p>
+ * The following convenience methods are also provided for setting context properties:
+ * <ul>
+ * <li>{@link HtmlParser#setProperty(String,Object)}
+ * <li>{@link HtmlParser#setProperties(ObjectMap)}
+ * <li>{@link HtmlParser#addNotBeanClasses(Class[])}
+ * <li>{@link HtmlParser#addTransforms(Class[])}
+ * <li>{@link HtmlParser#addImplClass(Class,Class)}
+ * </ul>
+ * <p>
+ * See {@link ContextFactory} for more information about context properties.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class HtmlParserContext extends ParserContext {
+
+ /**
+ * Constructor.
+ * <p>
+ * Typically only called from {@link ContextFactory#getContext(Class)}.
+ *
+ * @param cf The factory that created this context.
+ */
+ public HtmlParserContext(ContextFactory cf) {
+ super(cf);
+ }
+
+ /**
+ * Constructor.
+ * <p>
+ * Typically only called from {@link ContextFactory#getContext(Class)}.
+ *
+ * @param cf The factory that created this context.
+ */
+ HtmlParserSession createSession(BeanContext beanContext, Object input, ObjectMap op, Method javaMethod, Object outer) {
+ return new HtmlParserSession(this, beanContext, input, op, javaMethod, outer);
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserSession.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserSession.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserSession.java
new file mode 100644
index 0000000..c2a24f5
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlParserSession.java
@@ -0,0 +1,86 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import javax.xml.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link HtmlParser}.
+ * <p>
+ * This class is NOT thread safe. It is meant to be discarded after one-time use.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class HtmlParserSession extends ParserSession {
+
+ private XMLEventReader xmlEventReader;
+
+ /**
+ * Create a new session using properties specified in the context.
+ *
+ * @param ctx The context creating this session object.
+ * The context contains all the configuration settings for this object.
+ * @param beanContext The bean context being used.
+ * @param input The input. Can be any of the following types:
+ * <ul>
+ * <li><jk>null</jk>
+ * <li>{@link Reader}
+ * <li>{@link CharSequence}
+ * <li>{@link InputStream} containing UTF-8 encoded text.
+ * <li>{@link File} containing system encoded text.
+ * </ul>
+ * @param op The override properties.
+ * These override any context properties defined in the context.
+ * @param javaMethod The java method that called this parser, usually the method in a REST servlet.
+ * @param outer The outer object for instantiating top-level non-static inner classes.
+ */
+ public HtmlParserSession(HtmlParserContext ctx, BeanContext beanContext, Object input, ObjectMap op, Method javaMethod, Object outer) {
+ super(ctx, beanContext, input, op, javaMethod, outer);
+ }
+
+ /**
+ * Wraps the specified reader in an {@link XMLEventReader}.
+ * This event reader gets closed by the {@link #close()} method.
+ *
+ * @param in The reader to read from.
+ * @param estimatedSize The estimated size of the input. If <code>-1</code>, uses a default size of <code>8196</code>.
+ * @return A new XML event reader using a new {@link XMLInputFactory}.
+ * @throws ParseException
+ */
+ final XMLEventReader getXmlEventReader() throws Exception {
+ Reader r = IOUtils.getBufferedReader(super.getReader());
+ XMLInputFactory factory = XMLInputFactory.newInstance();
+ factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
+ this.xmlEventReader = factory.createXMLEventReader(r);
+ return xmlEventReader;
+ }
+
+ @Override /* ParserSession */
+ public void close() throws ParseException {
+ if (xmlEventReader != null) {
+ try {
+ xmlEventReader.close();
+ } catch (XMLStreamException e) {
+ throw new ParseException(e);
+ }
+ }
+ super.close();
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
new file mode 100644
index 0000000..99957ff
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
@@ -0,0 +1,154 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.serializer.SerializerContext.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.transform.*;
+
+/**
+ * Serializes POJO metamodels to HTML.
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ * Handles <code>Accept</code> types: <code>text/html+schema</code>
+ * <p>
+ * Produces <code>Content-Type</code> types: <code>text/html</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ * Essentially the same as {@link HtmlSerializer}, except serializes the POJO metamodel
+ * instead of the model itself.
+ * <p>
+ * Produces output that describes the POJO metamodel similar to an XML schema document.
+ * <p>
+ * The easiest way to create instances of this class is through the {@link HtmlSerializer#getSchemaSerializer()},
+ * which will create a schema serializer with the same settings as the originating serializer.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@Produces(value="text/html+schema", contentType="text/html")
+public final class HtmlSchemaDocSerializer extends HtmlDocSerializer {
+
+ /**
+ * Constructor.
+ */
+ public HtmlSchemaDocSerializer() {
+ setProperty(SERIALIZER_detectRecursions, true);
+ setProperty(SERIALIZER_ignoreRecursions, true);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cf The context factory to use for creating the context for this serializer.
+ */
+ public HtmlSchemaDocSerializer(ContextFactory cf) {
+ getContextFactory().copyFrom(cf);
+ setProperty(SERIALIZER_detectRecursions, true);
+ setProperty(SERIALIZER_ignoreRecursions, true);
+ }
+
+ @Override /* Serializer */
+ public HtmlDocSerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) {
+ return new HtmlDocSerializerSession(getContext(HtmlDocSerializerContext.class), getBeanContext(), output, properties, javaMethod);
+ }
+
+ @Override /* ISchemaSerializer */
+ protected void doSerialize(SerializerSession session, Object o) throws Exception {
+ HtmlSerializerSession s = (HtmlSerializerSession)session;
+ ObjectMap schema = getSchema(s, s.getBeanContext().getClassMetaForObject(o), "root", null);
+ super.doSerialize(s, schema);
+ }
+
+ /*
+ * Creates a schema representation of the specified class type.
+ *
+ * @param eType The class type to get the schema of.
+ * @param ctx Serialize context used to prevent infinite loops.
+ * @param attrName The name of the current attribute.
+ * @return A schema representation of the specified class.
+ * @throws SerializeException If a problem occurred trying to convert the output.
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private ObjectMap getSchema(HtmlSerializerSession session, ClassMeta<?> eType, String attrName, String[] pNames) throws Exception {
+
+ ObjectMap out = new ObjectMap();
+
+ ClassMeta<?> aType; // The actual type (will be null if recursion occurs)
+ ClassMeta<?> gType; // The generic type
+
+ aType = session.push(attrName, eType, null);
+
+ gType = eType.getTransformedClassMeta();
+ String type = null;
+
+ if (gType.isEnum() || gType.isCharSequence() || gType.isChar())
+ type = "string";
+ else if (gType.isNumber())
+ type = "number";
+ else if (gType.isBoolean())
+ type = "boolean";
+ else if (gType.isBean() || gType.isMap())
+ type = "object";
+ else if (gType.isCollection() || gType.isArray())
+ type = "array";
+ else
+ type = "any";
+
+ out.put("type", type);
+ out.put("class", eType.toString());
+ PojoTransform t = eType.getPojoTransform();
+ if (t != null)
+ out.put("transform", t);
+
+ if (aType != null) {
+ if (gType.isEnum())
+ out.put("enum", getEnumStrings((Class<Enum<?>>)gType.getInnerClass()));
+ else if (gType.isCollection() || gType.isArray()) {
+ ClassMeta componentType = gType.getElementType();
+ if (gType.isCollection() && isParentClass(Set.class, gType.getInnerClass()))
+ out.put("uniqueItems", true);
+ out.put("items", getSchema(session, componentType, "items", pNames));
+ } else if (gType.isBean()) {
+ ObjectMap properties = new ObjectMap();
+ BeanMeta bm = session.getBeanContext().getBeanMeta(gType.getInnerClass());
+ if (pNames != null)
+ bm = new BeanMetaFiltered(bm, pNames);
+ for (Iterator<BeanPropertyMeta<?>> i = bm.getPropertyMetas().iterator(); i.hasNext();) {
+ BeanPropertyMeta p = i.next();
+ properties.put(p.getName(), getSchema(session, p.getClassMeta(), p.getName(), p.getProperties()));
+ }
+ out.put("properties", properties);
+ }
+ }
+ session.pop();
+ return out;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private List<String> getEnumStrings(Class<? extends Enum> c) {
+ List<String> l = new LinkedList<String>();
+ for (Object e : EnumSet.allOf(c))
+ l.add(e.toString());
+ return l;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializer.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializer.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializer.java
new file mode 100644
index 0000000..659e18e
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializer.java
@@ -0,0 +1,645 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import static org.apache.juneau.serializer.SerializerContext.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.transform.*;
+import org.apache.juneau.xml.*;
+import org.apache.juneau.xml.annotation.*;
+
+/**
+ * Serializes POJO models to HTML.
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ * Handles <code>Accept</code> types: <code>text/html</code>
+ * <p>
+ * Produces <code>Content-Type</code> types: <code>text/html</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ * The conversion is as follows...
+ * <ul class='spaced-list'>
+ * <li>{@link Map Maps} (e.g. {@link HashMap}, {@link TreeMap}) and beans are converted to HTML tables with 'key' and 'value' columns.
+ * <li>{@link Collection Collections} (e.g. {@link HashSet}, {@link LinkedList}) and Java arrays are converted to HTML ordered lists.
+ * <li>{@code Collections} of {@code Maps} and beans are converted to HTML tables with keys as headers.
+ * <li>Everything else is converted to text.
+ * </ul>
+ * <p>
+ * This serializer provides several serialization options. Typically, one of the predefined <jsf>DEFAULT</jsf> serializers will be sufficient.
+ * However, custom serializers can be constructed to fine-tune behavior.
+ * <p>
+ * The {@link HtmlLink} annotation can be used on beans to add hyperlinks to the output.
+ *
+ *
+ * <h6 class='topic'>Configurable properties</h6>
+ * <p>
+ * This class has the following properties associated with it:
+ * <ul class='spaced-list'>
+ * <li>{@link HtmlSerializerContext}
+ * </ul>
+ *
+ *
+ * <h6 class='topic'>Behavior-specific subclasses</h6>
+ * <p>
+ * The following direct subclasses are provided for convenience:
+ * <ul class='spaced-list'>
+ * <li>{@link Sq} - Default serializer, single quotes.
+ * <li>{@link SqReadable} - Default serializer, single quotes, whitespace added.
+ * </ul>
+ *
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ * <jc>// Use one of the default serializers to serialize a POJO</jc>
+ * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(someObject);
+ *
+ * <jc>// Create a custom serializer that doesn't use whitespace and newlines</jc>
+ * HtmlSerializer serializer = <jk>new</jk> HtmlSerializer()
+ * .setProperty(SerializerContext.<jsf>SERIALIZER_useIndentation</jsf>, <jk>false</jk>);
+ *
+ * <jc>// Same as above, except uses cloning</jc>
+ * HtmlSerializer serializer = HtmlSerializer.<jsf>DEFAULT</jsf>.clone()
+ * .setProperty(SerializerContext.<jsf>SERIALIZER_useIndentation</jsf>, <jk>false</jk>);
+ *
+ * <jc>// Serialize POJOs to HTML</jc>
+ *
+ * <jc>// Produces: </jc>
+ * <jc>// <ul><li>1<li>2<li>3</ul></jc>
+ * List l = new ObjectList(1, 2, 3);
+ * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(l);
+ *
+ * <jc>// Produces: </jc>
+ * <jc>// <table> </jc>
+ * <jc>// <tr><th>firstName</th><th>lastName</th></tr> </jc>
+ * <jc>// <tr><td>Bob</td><td>Costas</td></tr> </jc>
+ * <jc>// <tr><td>Billy</td><td>TheKid</td></tr> </jc>
+ * <jc>// <tr><td>Barney</td><td>Miller</td></tr> </jc>
+ * <jc>// </table> </jc>
+ * l = <jk>new</jk> ObjectList();
+ * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Bob',lastName:'Costas'}"</js>));
+ * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Billy',lastName:'TheKid'}"</js>));
+ * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Barney',lastName:'Miller'}"</js>));
+ * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(l);
+ *
+ * <jc>// Produces: </jc>
+ * <jc>// <table> </jc>
+ * <jc>// <tr><th>key</th><th>value</th></tr> </jc>
+ * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc>
+ * <jc>// <tr><td>baz</td><td>123</td></tr> </jc>
+ * <jc>// </table> </jc>
+ * Map m = <jk>new</jk> ObjectMap(<js>"{foo:'bar',baz:123}"</js>);
+ * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(m);
+ *
+ * <jc>// HTML elements can be nested arbitrarily deep</jc>
+ * <jc>// Produces: </jc>
+ * <jc>// <table> </jc>
+ * <jc>// <tr><th>key</th><th>value</th></tr> </jc>
+ * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc>
+ * <jc>// <tr><td>baz</td><td>123</td></tr> </jc>
+ * <jc>// <tr><td>someNumbers</td><td><ul><li>1<li>2<li>3</ul></td></tr> </jc>
+ * <jc>// <tr><td>someSubMap</td><td> </jc>
+ * <jc>// <table> </jc>
+ * <jc>// <tr><th>key</th><th>value</th></tr> </jc>
+ * <jc>// <tr><td>a</td><td>b</td></tr> </jc>
+ * <jc>// </table> </jc>
+ * <jc>// </td></tr> </jc>
+ * <jc>// </table> </jc>
+ * Map m = <jk>new</jk> ObjectMap(<js>"{foo:'bar',baz:123}"</js>);
+ * m.put("someNumbers", new ObjectList(1, 2, 3));
+ * m.put(<js>"someSubMap"</js>, new ObjectMap(<js>"{a:'b'}"</js>));
+ * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(m);
+ * </p>
+ *
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@Produces("text/html")
+@SuppressWarnings("hiding")
+public class HtmlSerializer extends XmlSerializer {
+
+ /** Default serializer, all default settings. */
+ public static final HtmlSerializer DEFAULT = new HtmlSerializer().lock();
+
+ /** Default serializer, single quotes. */
+ public static final HtmlSerializer DEFAULT_SQ = new HtmlSerializer.Sq().lock();
+
+ /** Default serializer, single quotes, whitespace added. */
+ public static final HtmlSerializer DEFAULT_SQ_READABLE = new HtmlSerializer.SqReadable().lock();
+
+ /** Default serializer, single quotes. */
+ public static class Sq extends HtmlSerializer {
+ /** Constructor */
+ public Sq() {
+ setProperty(SERIALIZER_quoteChar, '\'');
+ }
+ }
+
+ /** Default serializer, single quotes, whitespace added. */
+ public static class SqReadable extends Sq {
+ /** Constructor */
+ public SqReadable() {
+ setProperty(SERIALIZER_useIndentation, true);
+ }
+ }
+
+ /**
+ * Main serialization routine.
+ * @param session The serialization context object.
+ * @param o The object being serialized.
+ * @param w The writer to serialize to.
+ *
+ * @return The same writer passed in.
+ * @throws IOException If a problem occurred trying to send output to the writer.
+ */
+ private HtmlWriter doSerialize(HtmlSerializerSession session, Object o, HtmlWriter w) throws Exception {
+ serializeAnything(session, w, o, null, null, session.getInitialDepth()-1, null);
+ return w;
+ }
+
+ /**
+ * Serialize the specified object to the specified writer.
+ *
+ * @param session The context object that lives for the duration of this serialization.
+ * @param out The writer.
+ * @param o The object to serialize.
+ * @param eType The expected type of the object if this is a bean property.
+ * @param name The attribute name of this object if this object was a field in a JSON object (i.e. key of a {@link java.util.Map.Entry} or property name of a bean).
+ * @param indent The current indentation value.
+ * @param pMeta The bean property being serialized, or <jk>null</jk> if we're not serializing a bean property.
+ *
+ * @throws Exception If a problem occurred trying to convert the output.
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ protected void serializeAnything(HtmlSerializerSession session, HtmlWriter out, Object o, ClassMeta<?> eType, String name, int indent, BeanPropertyMeta pMeta) throws Exception {
+
+ BeanContext bc = session.getBeanContext();
+ ClassMeta<?> aType = null; // The actual type
+ ClassMeta<?> gType = object(); // The generic type
+
+ if (eType == null)
+ eType = object();
+
+ aType = session.push(name, o, eType);
+
+ // Handle recursion
+ if (aType == null) {
+ o = null;
+ aType = object();
+ }
+
+ session.indent += indent;
+ int i = session.indent;
+
+ // Determine the type.
+ if (o == null || (aType.isChar() && ((Character)o).charValue() == 0))
+ out.tag(i, "null").nl();
+ else {
+
+ gType = aType.getTransformedClassMeta();
+ String classAttr = null;
+ if (session.isAddClassAttrs() && ! eType.equals(aType))
+ classAttr = aType.toString();
+
+ // Transform if necessary
+ PojoTransform transform = aType.getPojoTransform();
+ if (transform != null) {
+ o = transform.transform(o);
+
+ // If the transforms getTransformedClass() method returns Object, we need to figure out
+ // the actual type now.
+ if (gType.isObject())
+ gType = bc.getClassMetaForObject(o);
+ }
+
+ HtmlClassMeta html = gType.getHtmlMeta();
+
+ if (html.isAsXml() || (pMeta != null && pMeta.getHtmlMeta().isAsXml()))
+ super.serializeAnything(session, out, o, null, null, null, false, XmlFormat.NORMAL, null);
+ else if (html.isAsPlainText() || (pMeta != null && pMeta.getHtmlMeta().isAsPlainText()))
+ out.write(o == null ? "null" : o.toString());
+ else if (o == null || (gType.isChar() && ((Character)o).charValue() == 0))
+ out.tag(i, "null").nl();
+ else if (gType.hasToObjectMapMethod())
+ serializeMap(session, out, gType.toObjectMap(o), eType, classAttr, pMeta);
+ else if (gType.isBean())
+ serializeBeanMap(session, out, bc.forBean(o), classAttr, pMeta);
+ else if (gType.isNumber())
+ out.sTag(i, "number").append(o).eTag("number").nl();
+ else if (gType.isBoolean())
+ out.sTag(i, "boolean").append(o).eTag("boolean").nl();
+ else if (gType.isMap()) {
+ if (o instanceof BeanMap)
+ serializeBeanMap(session, out, (BeanMap)o, classAttr, pMeta);
+ else
+ serializeMap(session, out, (Map)o, eType, classAttr, pMeta);
+ }
+ else if (gType.isCollection()) {
+ if (classAttr != null)
+ serializeCollection(session, out, (Collection)o, gType, name, classAttr, pMeta);
+ else
+ serializeCollection(session, out, (Collection)o, eType, name, null, pMeta);
+ }
+ else if (gType.isArray()) {
+ if (classAttr != null)
+ serializeCollection(session, out, toList(gType.getInnerClass(), o), gType, name, classAttr, pMeta);
+ else
+ serializeCollection(session, out, toList(gType.getInnerClass(), o), eType, name, null, pMeta);
+ }
+ else if (session.isUri(gType, pMeta, o)) {
+ String label = session.getAnchorText(pMeta, o);
+ out.oTag(i, "a").attrUri("href", o).append('>');
+ out.append(label);
+ out.eTag("a").nl();
+ }
+ else
+ out.sTag(i, "string").encodeText(session.toString(o)).eTag("string").nl();
+ }
+ session.pop();
+ session.indent -= indent;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private void serializeMap(HtmlSerializerSession session, HtmlWriter out, Map m, ClassMeta<?> type, String classAttr, BeanPropertyMeta<?> ppMeta) throws Exception {
+ ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType();
+ ClassMeta<?> aType = session.getBeanContext().getClassMetaForObject(m); // The actual type
+
+ int i = session.getIndent();
+ out.oTag(i, "table").attr("type", "object");
+ if (classAttr != null)
+ out.attr("class", classAttr);
+ out.appendln(">");
+ if (! (aType.getHtmlMeta().isNoTableHeaders() || (ppMeta != null && ppMeta.getHtmlMeta().isNoTableHeaders()))) {
+ out.sTag(i+1, "tr").nl();
+ out.sTag(i+2, "th").nl().appendln(i+3, "<string>key</string>").eTag(i+2, "th").nl();
+ out.sTag(i+2, "th").nl().appendln(i+3, "<string>value</string>").eTag(i+2, "th").nl();
+ out.eTag(i+1, "tr").nl();
+ }
+ for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
+
+ Object key = session.generalize(e.getKey(), keyType);
+ Object value = null;
+ try {
+ value = e.getValue();
+ } catch (StackOverflowError t) {
+ throw t;
+ } catch (Throwable t) {
+ session.addWarning("Could not call getValue() on property ''{0}'', {1}", e.getKey(), t.getLocalizedMessage());
+ }
+
+ out.sTag(i+1, "tr").nl();
+ out.sTag(i+2, "td").nl();
+ serializeAnything(session, out, key, keyType, null, 2, null);
+ out.eTag(i+2, "td").nl();
+ out.sTag(i+2, "td").nl();
+ serializeAnything(session, out, value, valueType, (key == null ? "_x0000_" : key.toString()), 2, null);
+ out.eTag(i+2, "td").nl();
+ out.eTag(i+1, "tr").nl();
+ }
+ out.eTag(i, "table").nl();
+ }
+
+ @SuppressWarnings({ "rawtypes" })
+ private void serializeBeanMap(HtmlSerializerSession session, HtmlWriter out, BeanMap m, String classAttr, BeanPropertyMeta<?> ppMeta) throws Exception {
+ int i = session.getIndent();
+
+ Object o = m.getBean();
+
+ Class<?> c = o.getClass();
+ if (c.isAnnotationPresent(HtmlLink.class)) {
+ HtmlLink h = o.getClass().getAnnotation(HtmlLink.class);
+ Object urlProp = m.get(h.hrefProperty());
+ Object nameProp = m.get(h.nameProperty());
+ out.oTag(i, "a").attrUri("href", urlProp).append('>').encodeText(nameProp).eTag("a").nl();
+ return;
+ }
+
+ out.oTag(i, "table").attr("type", "object");
+ if (classAttr != null)
+ out.attr("_class", classAttr);
+ out.append('>').nl();
+ if (! (m.getClassMeta().getHtmlMeta().isNoTableHeaders() || (ppMeta != null && ppMeta.getHtmlMeta().isNoTableHeaders()))) {
+ out.sTag(i+1, "tr").nl();
+ out.sTag(i+2, "th").nl().appendln(i+3, "<string>key</string>").eTag(i+2, "th").nl();
+ out.sTag(i+2, "th").nl().appendln(i+3, "<string>value</string>").eTag(i+2, "th").nl();
+ out.eTag(i+1, "tr").nl();
+ }
+
+ Iterator mapEntries = m.entrySet(session.isTrimNulls()).iterator();
+
+ while (mapEntries.hasNext()) {
+ BeanMapEntry p = (BeanMapEntry)mapEntries.next();
+ BeanPropertyMeta pMeta = p.getMeta();
+
+ String key = p.getKey();
+ Object value = null;
+ try {
+ value = p.getValue();
+ } catch (StackOverflowError e) {
+ throw e;
+ } catch (Throwable t) {
+ session.addBeanGetterWarning(pMeta, t);
+ }
+
+ if (session.canIgnoreValue(pMeta.getClassMeta(), key, value))
+ continue;
+
+ out.sTag(i+1, "tr").nl();
+ out.sTag(i+2, "td").nl();
+ out.sTag(i+3, "string").encodeText(key).eTag("string").nl();
+ out.eTag(i+2, "td").nl();
+ out.sTag(i+2, "td").nl();
+ try {
+ serializeAnything(session, out, value, p.getMeta().getClassMeta(), key, 2, pMeta);
+ } catch (SerializeException t) {
+ throw t;
+ } catch (StackOverflowError t) {
+ throw t;
+ } catch (Throwable t) {
+ session.addBeanGetterWarning(pMeta, t);
+ }
+ out.eTag(i+2, "td").nl();
+ out.eTag(i+1, "tr").nl();
+ }
+ out.eTag(i, "table").nl();
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private void serializeCollection(HtmlSerializerSession session, HtmlWriter out, Collection c, ClassMeta<?> type, String name, String classAttr, BeanPropertyMeta<?> ppMeta) throws Exception {
+
+ BeanContext bc = session.getBeanContext();
+ ClassMeta<?> elementType = type.getElementType();
+
+ int i = session.getIndent();
+ if (c.isEmpty()) {
+ out.appendln(i, "<ul></ul>");
+ return;
+ }
+
+ c = session.sort(c);
+
+ // Look at the objects to see how we're going to handle them. Check the first object to see how we're going to handle this.
+ // If it's a map or bean, then we'll create a table.
+ // Otherwise, we'll create a list.
+ String[] th = getTableHeaders(session, c, ppMeta);
+
+ if (th != null) {
+
+ out.oTag(i, "table").attr("type", "array");
+ if (classAttr != null)
+ out.attr("_class", classAttr);
+ out.append('>').nl();
+ out.sTag(i+1, "tr").nl();
+ for (String key : th)
+ out.sTag(i+2, "th").append(key).eTag("th").nl();
+ out.eTag(i+1, "tr").nl();
+
+ for (Object o : c) {
+ ClassMeta<?> cm = bc.getClassMetaForObject(o);
+
+ if (cm != null && cm.getPojoTransform() != null) {
+ PojoTransform f = cm.getPojoTransform();
+ o = f.transform(o);
+ cm = cm.getTransformedClassMeta();
+ }
+
+ if (cm != null && session.isAddClassAttrs() && elementType.getInnerClass() != o.getClass())
+ out.oTag(i+1, "tr").attr("_class", o.getClass().getName()).append('>').nl();
+ else
+ out.sTag(i+1, "tr").nl();
+
+ if (cm == null) {
+ serializeAnything(session, out, o, null, null, 1, null);
+
+ } else if (cm.isMap() && ! (cm.isBeanMap())) {
+ Map m2 = session.sort((Map)o);
+
+ Iterator mapEntries = m2.entrySet().iterator();
+ while (mapEntries.hasNext()) {
+ Map.Entry e = (Map.Entry)mapEntries.next();
+ out.sTag(i+2, "td").nl();
+ serializeAnything(session, out, e.getValue(), elementType, e.getKey().toString(), 2, null);
+ out.eTag(i+2, "td").nl();
+ }
+ } else {
+ BeanMap m2 = null;
+ if (o instanceof BeanMap)
+ m2 = (BeanMap)o;
+ else
+ m2 = bc.forBean(o);
+
+ Iterator mapEntries = m2.entrySet(session.isTrimNulls()).iterator();
+ while (mapEntries.hasNext()) {
+ BeanMapEntry p = (BeanMapEntry)mapEntries.next();
+ BeanPropertyMeta pMeta = p.getMeta();
+ out.sTag(i+2, "td").nl();
+ serializeAnything(session, out, p.getValue(), pMeta.getClassMeta(), p.getKey().toString(), 2, pMeta);
+ out.eTag(i+2, "td").nl();
+ }
+ }
+ out.eTag(i+1, "tr").nl();
+ }
+ out.eTag(i, "table").nl();
+
+ } else {
+ out.sTag(i, "ul").nl();
+ for (Object o : c) {
+ out.sTag(i+1, "li").nl();
+ serializeAnything(session, out, o, elementType, name, 1, null);
+ out.eTag(i+1, "li").nl();
+ }
+ out.eTag(i, "ul").nl();
+ }
+ }
+
+ /*
+ * Returns the table column headers for the specified collection of objects.
+ * Returns null if collection should not be serialized as a 2-dimensional table.
+ * 2-dimensional tables are used for collections of objects that all have the same set of property names.
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private String[] getTableHeaders(SerializerSession session, Collection c, BeanPropertyMeta<?> pMeta) throws Exception {
+ BeanContext bc = session.getBeanContext();
+ if (c.size() == 0)
+ return null;
+ c = session.sort(c);
+ String[] th;
+ Set<String> s = new TreeSet<String>();
+ Set<ClassMeta> prevC = new HashSet<ClassMeta>();
+ Object o1 = null;
+ for (Object o : c)
+ if (o != null) {
+ o1 = o;
+ break;
+ }
+ if (o1 == null)
+ return null;
+ ClassMeta cm = bc.getClassMetaForObject(o1);
+ if (cm.getPojoTransform() != null) {
+ PojoTransform f = cm.getPojoTransform();
+ o1 = f.transform(o1);
+ cm = cm.getTransformedClassMeta();
+ }
+ if (cm == null || ! (cm.isMap() || cm.isBean()))
+ return null;
+ if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class))
+ return null;
+ HtmlClassMeta h = cm.getHtmlMeta();
+ if (h.isNoTables() || (pMeta != null && pMeta.getHtmlMeta().isNoTables()))
+ return null;
+ if (h.isNoTableHeaders() || (pMeta != null && pMeta.getHtmlMeta().isNoTableHeaders()))
+ return new String[0];
+ if (session.canIgnoreValue(cm, null, o1))
+ return null;
+ if (cm.isMap() && ! cm.isBeanMap()) {
+ Map m = (Map)o1;
+ th = new String[m.size()];
+ int i = 0;
+ for (Object k : m.keySet())
+ th[i++] = (k == null ? null : k.toString());
+ } else {
+ BeanMap<?> bm = (o1 instanceof BeanMap ? (BeanMap)o1 : bc.forBean(o1));
+ List<String> l = new LinkedList<String>();
+ for (String k : bm.keySet())
+ l.add(k);
+ th = l.toArray(new String[l.size()]);
+ }
+ prevC.add(cm);
+ s.addAll(Arrays.asList(th));
+
+ for (Object o : c) {
+ if (o == null)
+ continue;
+ cm = bc.getClassMetaForObject(o);
+ if (cm != null && cm.getPojoTransform() != null) {
+ PojoTransform f = cm.getPojoTransform();
+ o = f.transform(o);
+ cm = cm.getTransformedClassMeta();
+ }
+ if (prevC.contains(cm))
+ continue;
+ if (cm == null || ! (cm.isMap() || cm.isBean()))
+ return null;
+ if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class))
+ return null;
+ if (session.canIgnoreValue(cm, null, o))
+ return null;
+ if (cm.isMap() && ! cm.isBeanMap()) {
+ Map m = (Map)o;
+ if (th.length != m.keySet().size())
+ return null;
+ for (Object k : m.keySet())
+ if (! s.contains(k.toString()))
+ return null;
+ } else {
+ BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : bc.forBean(o));
+ int l = 0;
+ for (String k : bm.keySet()) {
+ if (! s.contains(k))
+ return null;
+ l++;
+ }
+ if (s.size() != l)
+ return null;
+ }
+ }
+ return th;
+ }
+
+ /**
+ * Returns the schema serializer based on the settings of this serializer.
+ * @return The schema serializer.
+ */
+ @Override /* XmlSerializer */
+ public HtmlSerializer getSchemaSerializer() {
+ try {
+ return new HtmlSchemaDocSerializer(getContextFactory().clone());
+ } catch (CloneNotSupportedException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ //--------------------------------------------------------------------------------
+ // Overridden methods
+ //--------------------------------------------------------------------------------
+
+ @Override /* Serializer */
+ public HtmlSerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) {
+ return new HtmlSerializerSession(getContext(HtmlSerializerContext.class), getBeanContext(), output, properties, javaMethod);
+ }
+
+ @Override /* Serializer */
+ protected void doSerialize(SerializerSession session, Object o) throws Exception {
+ HtmlSerializerSession s = (HtmlSerializerSession)session;
+ doSerialize(s, o, s.getWriter());
+ }
+
+ @Override /* CoreApi */
+ public HtmlSerializer setProperty(String property, Object value) throws LockedException {
+ super.setProperty(property, value);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlSerializer setProperties(ObjectMap properties) throws LockedException {
+ super.setProperties(properties);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlSerializer addNotBeanClasses(Class<?>...classes) throws LockedException {
+ super.addNotBeanClasses(classes);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlSerializer addTransforms(Class<?>...classes) throws LockedException {
+ super.addTransforms(classes);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public <T> HtmlSerializer addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException {
+ super.addImplClass(interfaceClass, implClass);
+ return this;
+ }
+
+ @Override /* CoreApi */
+ public HtmlSerializer setClassLoader(ClassLoader classLoader) throws LockedException {
+ super.setClassLoader(classLoader);
+ return this;
+ }
+
+ @Override /* Lockable */
+ public HtmlSerializer lock() {
+ super.lock();
+ return this;
+ }
+
+ @Override /* Lockable */
+ public HtmlSerializer clone() {
+ HtmlSerializer c = (HtmlSerializer)super.clone();
+ return c;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java
new file mode 100644
index 0000000..740a16a
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java
@@ -0,0 +1,108 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import org.apache.juneau.*;
+import org.apache.juneau.xml.*;
+
+/**
+ * Configurable properties on the {@link HtmlSerializer} class.
+ * <p>
+ * Context properties are set by calling {@link ContextFactory#setProperty(String, Object)} on the context factory
+ * returned {@link CoreApi#getContextFactory()}.
+ * <p>
+ * The following convenience methods are also provided for setting context properties:
+ * <ul>
+ * <li>{@link HtmlSerializer#setProperty(String,Object)}
+ * <li>{@link HtmlSerializer#setProperties(ObjectMap)}
+ * <li>{@link HtmlSerializer#addNotBeanClasses(Class[])}
+ * <li>{@link HtmlSerializer#addTransforms(Class[])}
+ * <li>{@link HtmlSerializer#addImplClass(Class,Class)}
+ * </ul>
+ * <p>
+ * See {@link ContextFactory} for more information about context properties.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public class HtmlSerializerContext extends XmlSerializerContext {
+
+ /**
+ * Anchor text source ({@link String}, default={@link #TO_STRING}).
+ * <p>
+ * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs><xt>></xt>text<xt></a></xt></code>)
+ * in HTML, this setting defines what to set the inner text to.
+ * <p>
+ * Possible values:
+ * <ul class='spaced-list'>
+ * <li>{@link #TO_STRING} / <js>"toString"</js> - Set to whatever is returned by {@link #toString()} on the object.
+ * <li>{@link #URI} / <js>"uri"</js> - Set to the URI value.
+ * <li>{@link #LAST_TOKEN} / <js>"lastToken"</js> - Set to the last token of the URI value.
+ * <li>{@link #PROPERTY_NAME} / <js>"propertyName"</js> - Set to the bean property name.
+ * <li>{@link #URI_ANCHOR} / <js>"uriAnchor"</js> - Set to the anchor of the URL. (e.g. <js>"http://localhost:9080/foobar#anchorTextHere"</js>)
+ * </ul>
+ */
+ public static final String HTML_uriAnchorText = "HtmlSerializer.uriAnchorText";
+
+ /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */
+ public static final String PROPERTY_NAME = "PROPERTY_NAME";
+ /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */
+ public static final String TO_STRING = "TO_STRING";
+ /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */
+ public static final String URI = "URI";
+ /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */
+ public static final String LAST_TOKEN = "LAST_TOKEN";
+ /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */
+ public static final String URI_ANCHOR = "URI_ANCHOR";
+
+
+ /**
+ * Look for URLs in {@link String Strings} ({@link Boolean}, default=<jk>true</jk>).
+ * <p>
+ * If a string looks like a URL (e.g. starts with <js>"http://"</js> or <js>"https://"</js>, then treat it like a URL
+ * and make it into a hyperlink based on the rules specified by {@link #HTML_uriAnchorText}.
+ */
+ public static final String HTML_detectLinksInStrings = "HtmlSerializer.detectLinksInStrings";
+
+ /**
+ * Look for link labels in the <js>"label"</js> parameter of the URL ({@link Boolean}, default=<jk>true</jk>).
+ * <p>
+ * If the URL has a label parameter (e.g. <js>"?label=foobar"</js>), then use that as the anchor text of the link.
+ * <p>
+ * The parameter name can be changed via the {@link #HTML_labelParameter} property.
+ */
+ public static final String HTML_lookForLabelParameters = "HtmlSerializer.lookForLabelParameters";
+
+ /**
+ * The parameter name to use when using {@link #HTML_lookForLabelParameters} ({@link String}, default=<js>"label"</js>).
+ */
+ public static final String HTML_labelParameter = "HtmlSerializer.labelParameter";
+
+ final String uriAnchorText;
+ final boolean lookForLabelParameters, detectLinksInStrings;
+ final String labelParameter;
+
+ /**
+ * Constructor.
+ * <p>
+ * Typically only called from {@link ContextFactory#getContext(Class)}.
+ *
+ * @param cf The factory that created this context.
+ */
+ public HtmlSerializerContext(ContextFactory cf) {
+ super(cf);
+ uriAnchorText = cf.getProperty(HTML_uriAnchorText, String.class, TO_STRING);
+ lookForLabelParameters = cf.getProperty(HTML_lookForLabelParameters, Boolean.class, true);
+ detectLinksInStrings = cf.getProperty(HTML_detectLinksInStrings, Boolean.class, true);
+ labelParameter = cf.getProperty(HTML_labelParameter, String.class, "label");
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
new file mode 100644
index 0000000..18f95c8
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -0,0 +1,153 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import static org.apache.juneau.html.HtmlSerializerContext.*;
+
+import java.lang.reflect.*;
+import java.util.regex.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.xml.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link HtmlSerializer}.
+ * <p>
+ * This class is NOT thread safe. It is meant to be discarded after one-time use.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public class HtmlSerializerSession extends XmlSerializerSession {
+
+ private final AnchorText anchorText;
+ private final boolean detectLinksInStrings, lookForLabelParameters;
+ private final Pattern urlPattern = Pattern.compile("http[s]?\\:\\/\\/.*");
+ private final Pattern labelPattern;
+ private final String absolutePathUriBase, relativeUriBase;
+
+
+ @SuppressWarnings("hiding")
+ enum AnchorText {
+ PROPERTY_NAME, TO_STRING, URI, LAST_TOKEN, URI_ANCHOR
+ }
+
+ /**
+ * Create a new session using properties specified in the context.
+ *
+ * @param ctx The context creating this session object.
+ * The context contains all the configuration settings for this object.
+ * @param beanContext The bean context being used.
+ * @param output The output object. See {@link JsonSerializerSession#getWriter()} for valid class types.
+ * @param op The override properties.
+ * These override any context properties defined in the context.
+ * @param javaMethod The java method that called this parser, usually the method in a REST servlet.
+ */
+ protected HtmlSerializerSession(HtmlSerializerContext ctx, BeanContext beanContext, Object output, ObjectMap op, Method javaMethod) {
+ super(ctx, beanContext, output, op, javaMethod);
+ String labelParameter;
+ if (op == null || op.isEmpty()) {
+ anchorText = Enum.valueOf(AnchorText.class, ctx.uriAnchorText);
+ detectLinksInStrings = ctx.detectLinksInStrings;
+ lookForLabelParameters = ctx.lookForLabelParameters;
+ labelParameter = ctx.labelParameter;
+ } else {
+ anchorText = Enum.valueOf(AnchorText.class, op.getString(HTML_uriAnchorText, ctx.uriAnchorText));
+ detectLinksInStrings = op.getBoolean(HTML_detectLinksInStrings, ctx.detectLinksInStrings);
+ lookForLabelParameters = op.getBoolean(HTML_lookForLabelParameters, ctx.lookForLabelParameters);
+ labelParameter = op.getString(HTML_labelParameter, ctx.labelParameter);
+ }
+ labelPattern = Pattern.compile("[\\?\\&]" + Pattern.quote(labelParameter) + "=([^\\&]*)");
+ this.absolutePathUriBase = getAbsolutePathUriBase();
+ this.relativeUriBase = getRelativeUriBase();
+ }
+
+ @Override /* XmlSerializerSession */
+ public HtmlWriter getWriter() throws Exception {
+ Object output = getOutput();
+ if (output instanceof HtmlWriter)
+ return (HtmlWriter)output;
+ return new HtmlWriter(super.getWriter(), isUseIndentation(), isTrimStrings(), getQuoteChar(), getRelativeUriBase(), getAbsolutePathUriBase());
+ }
+
+ /**
+ * Returns <jk>true</jk> if the specified object is a URL.
+ *
+ * @param cm The ClassMeta of the object being serialized.
+ * @param pMeta The property metadata of the bean property of the object. Can be <jk>null</jk> if the object isn't from a bean property.
+ * @param o The object.
+ * @return <jk>true</jk> if the specified object is a URL.
+ */
+ public boolean isUri(ClassMeta<?> cm, BeanPropertyMeta<?> pMeta, Object o) {
+ if (cm.isUri())
+ return true;
+ if (pMeta != null && (pMeta.isUri() || pMeta.isBeanUri()))
+ return true;
+ if (detectLinksInStrings && o instanceof CharSequence && urlPattern.matcher(o.toString()).matches())
+ return true;
+ return false;
+ }
+
+ /**
+ * Returns the anchor text to use for the specified URL object.
+ *
+ * @param pMeta The property metadata of the bean property of the object. Can be <jk>null</jk> if the object isn't from a bean property.
+ * @param o The URL object.
+ * @return The anchor text to use for the specified URL object.
+ */
+ public String getAnchorText(BeanPropertyMeta<?> pMeta, Object o) {
+ String s;
+ if (lookForLabelParameters) {
+ s = o.toString();
+ Matcher m = labelPattern.matcher(s);
+ if (m.find())
+ return m.group(1);
+ }
+ switch (anchorText) {
+ case LAST_TOKEN:
+ s = o.toString();
+ if (s.indexOf('/') != -1)
+ s = s.substring(s.lastIndexOf('/')+1);
+ if (s.indexOf('?') != -1)
+ s = s.substring(0, s.indexOf('?'));
+ if (s.indexOf('#') != -1)
+ s = s.substring(0, s.indexOf('#'));
+ return s;
+ case URI_ANCHOR:
+ s = o.toString();
+ if (s.indexOf('#') != -1)
+ s = s.substring(s.lastIndexOf('#')+1);
+ return s;
+ case PROPERTY_NAME:
+ return pMeta == null ? o.toString() : pMeta.getName();
+ case URI:
+ s = o.toString();
+ if (s.indexOf("://") == -1) {
+ if (StringUtils.startsWith(s, '/')) {
+ s = absolutePathUriBase + s;
+ } else {
+ if (relativeUriBase != null) {
+ if (! relativeUriBase.equals("/"))
+ s = relativeUriBase + "/" + s;
+ else
+ s = "/" + s;
+ }
+ }
+ }
+ return s;
+ default:
+ return o.toString();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
new file mode 100644
index 0000000..f25d858
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
@@ -0,0 +1,58 @@
+/***************************************************************************************************************************
+ * 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.juneau.html;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes POJOs to HTTP responses as stripped HTML.
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ * Handles <code>Accept</code> types: <code>text/html+stripped</code>
+ * <p>
+ * Produces <code>Content-Type</code> types: <code>text/html</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ * Produces the same output as {@link HtmlDocSerializer}, but without the header and body tags and page title and description.
+ * Used primarily for JUnit testing the {@link HtmlDocSerializer} class.
+ *
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+@Produces(value="text/html+stripped",contentType="text/html")
+public class HtmlStrippedDocSerializer extends HtmlSerializer {
+
+ //---------------------------------------------------------------------------
+ // Overridden methods
+ //---------------------------------------------------------------------------
+
+ @Override /* Serializer */
+ protected void doSerialize(SerializerSession session, Object o) throws Exception {
+ HtmlSerializerSession s = (HtmlSerializerSession)session;
+ HtmlWriter w = s.getWriter();
+ if (o == null
+ || (o instanceof Collection && ((Collection<?>)o).size() == 0)
+ || (o.getClass().isArray() && Array.getLength(o) == 0))
+ w.sTag(1, "p").append("No Results").eTag("p").nl();
+ else
+ super.doSerialize(s, o);
+ }
+}