http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParser.java
new file mode 100644
index 0000000..bb1951d
--- /dev/null
+++ b/juneau-core/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 ([email protected])
+ */
+@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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserContext.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserContext.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserContext.java
new file mode 100644
index 0000000..716cc0a
--- /dev/null
+++ b/juneau-core/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 ([email protected])
+ */
+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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlParserSession.java
new file mode 100644
index 0000000..c2a24f5
--- /dev/null
+++ b/juneau-core/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 ([email protected])
+ */
+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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
new file mode 100644
index 0000000..99957ff
--- /dev/null
+++ 
b/juneau-core/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 ([email protected])
+ */
+@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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
new file mode 100644
index 0000000..522e06a
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
@@ -0,0 +1,638 @@
+/***************************************************************************************************************************
+ * 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>// 
&lt;ul&gt;&lt;li&gt;1&lt;li&gt;2&lt;li&gt;3&lt;/ul&gt;</jc>
+ *             List l = new ObjectList(1, 2, 3);
+ *             String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(l);
+ *
+ *             <jc>// Produces: </jc>
+ *             <jc>//    &lt;table&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;th&gt;firstName&lt;/th&gt;&lt;th&gt;lastName&lt;/th&gt;&lt;/tr&gt;
 </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;td&gt;Bob&lt;/td&gt;&lt;td&gt;Costas&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;td&gt;Billy&lt;/td&gt;&lt;td&gt;TheKid&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;td&gt;Barney&lt;/td&gt;&lt;td&gt;Miller&lt;/td&gt;&lt;/tr&gt; 
</jc>
+ *             <jc>//    &lt;/table&gt; </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>//    &lt;table&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;td&gt;foo&lt;/td&gt;&lt;td&gt;bar&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//       
&lt;tr&gt;&lt;td&gt;baz&lt;/td&gt;&lt;td&gt;123&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//    &lt;/table&gt; </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>//  &lt;table&gt; </jc>
+ *             <jc>//          
&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt; </jc>
+ *             <jc>//          
&lt;tr&gt;&lt;td&gt;foo&lt;/td&gt;&lt;td&gt;bar&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//          
&lt;tr&gt;&lt;td&gt;baz&lt;/td&gt;&lt;td&gt;123&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//          
&lt;tr&gt;&lt;td&gt;someNumbers&lt;/td&gt;&lt;td&gt;&lt;ul&gt;&lt;li&gt;1&lt;li&gt;2&lt;li&gt;3&lt;/ul&gt;&lt;/td&gt;&lt;/tr&gt;
 </jc>
+ *             <jc>//          
&lt;tr&gt;&lt;td&gt;someSubMap&lt;/td&gt;&lt;td&gt; </jc>
+ *             <jc>//                  &lt;table&gt; </jc>
+ *             <jc>//                          
&lt;tr&gt;&lt;th&gt;key&lt;/th&gt;&lt;th&gt;value&lt;/th&gt;&lt;/tr&gt; </jc>
+ *             <jc>//                          
&lt;tr&gt;&lt;td&gt;a&lt;/td&gt;&lt;td&gt;b&lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//                  &lt;/table&gt; </jc>
+ *             <jc>//          &lt;/td&gt;&lt;/tr&gt; </jc>
+ *             <jc>//  &lt;/table&gt; </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 ([email protected])
+ */
+@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();
+               }
+
+               for (BeanPropertyValue p : m.getValues(false, 
session.isTrimNulls())) {
+                       BeanPropertyMeta pMeta = p.getMeta();
+
+                       String key = p.getName();
+                       Object value = p.getValue();
+                       Throwable t = p.getThrown();
+                       if (t != null)
+                               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 e) {
+                               throw e;
+                       } catch (Error e) {
+                               throw e;
+                       } catch (Throwable e) {
+                               session.addBeanGetterWarning(pMeta, e);
+                       }
+                       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().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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerContext.java
new file mode 100644
index 0000000..740a16a
--- /dev/null
+++ 
b/juneau-core/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 ([email protected])
+ */
+public class HtmlSerializerContext extends XmlSerializerContext {
+
+       /**
+        * Anchor text source ({@link String}, default={@link #TO_STRING}).
+        * <p>
+        * When creating anchor tags (e.g. <code><xt>&lt;a</xt> 
<xa>href</xa>=<xs>'...'</xs><xt>&gt;</xt>text<xt>&lt;/a&gt;</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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
new file mode 100644
index 0000000..18f95c8
--- /dev/null
+++ 
b/juneau-core/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 ([email protected])
+ */
+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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
 
b/juneau-core/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializer.java
new file mode 100644
index 0000000..f25d858
--- /dev/null
+++ 
b/juneau-core/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 ([email protected])
+ */
+@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);
+       }
+}

Reply via email to