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>// <ul><li>1<li>2<li>3</ul></jc> + * List l = new ObjectList(1, 2, 3); + * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(l); + * + * <jc>// Produces: </jc> + * <jc>// <table> </jc> + * <jc>// <tr><th>firstName</th><th>lastName</th></tr> </jc> + * <jc>// <tr><td>Bob</td><td>Costas</td></tr> </jc> + * <jc>// <tr><td>Billy</td><td>TheKid</td></tr> </jc> + * <jc>// <tr><td>Barney</td><td>Miller</td></tr> </jc> + * <jc>// </table> </jc> + * l = <jk>new</jk> ObjectList(); + * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Bob',lastName:'Costas'}"</js>)); + * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Billy',lastName:'TheKid'}"</js>)); + * l.add(<jk>new</jk> ObjectMap(<js>"{firstName:'Barney',lastName:'Miller'}"</js>)); + * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(l); + * + * <jc>// Produces: </jc> + * <jc>// <table> </jc> + * <jc>// <tr><th>key</th><th>value</th></tr> </jc> + * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc> + * <jc>// <tr><td>baz</td><td>123</td></tr> </jc> + * <jc>// </table> </jc> + * Map m = <jk>new</jk> ObjectMap(<js>"{foo:'bar',baz:123}"</js>); + * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(m); + * + * <jc>// HTML elements can be nested arbitrarily deep</jc> + * <jc>// Produces: </jc> + * <jc>// <table> </jc> + * <jc>// <tr><th>key</th><th>value</th></tr> </jc> + * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc> + * <jc>// <tr><td>baz</td><td>123</td></tr> </jc> + * <jc>// <tr><td>someNumbers</td><td><ul><li>1<li>2<li>3</ul></td></tr> </jc> + * <jc>// <tr><td>someSubMap</td><td> </jc> + * <jc>// <table> </jc> + * <jc>// <tr><th>key</th><th>value</th></tr> </jc> + * <jc>// <tr><td>a</td><td>b</td></tr> </jc> + * <jc>// </table> </jc> + * <jc>// </td></tr> </jc> + * <jc>// </table> </jc> + * Map m = <jk>new</jk> ObjectMap(<js>"{foo:'bar',baz:123}"</js>); + * m.put("someNumbers", new ObjectList(1, 2, 3)); + * m.put(<js>"someSubMap"</js>, new ObjectMap(<js>"{a:'b'}"</js>)); + * String html = HtmlSerializer.<jsf>DEFAULT</jsf>.serialize(m); + * </p> + * + * + * @author James Bognar ([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><a</xt> <xa>href</xa>=<xs>'...'</xs><xt>></xt>text<xt></a></xt></code>) + * in HTML, this setting defines what to set the inner text to. + * <p> + * Possible values: + * <ul class='spaced-list'> + * <li>{@link #TO_STRING} / <js>"toString"</js> - Set to whatever is returned by {@link #toString()} on the object. + * <li>{@link #URI} / <js>"uri"</js> - Set to the URI value. + * <li>{@link #LAST_TOKEN} / <js>"lastToken"</js> - Set to the last token of the URI value. + * <li>{@link #PROPERTY_NAME} / <js>"propertyName"</js> - Set to the bean property name. + * <li>{@link #URI_ANCHOR} / <js>"uriAnchor"</js> - Set to the anchor of the URL. (e.g. <js>"http://localhost:9080/foobar#anchorTextHere"</js>) + * </ul> + */ + public static final String HTML_uriAnchorText = "HtmlSerializer.uriAnchorText"; + + /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */ + public static final String PROPERTY_NAME = "PROPERTY_NAME"; + /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */ + public static final String TO_STRING = "TO_STRING"; + /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */ + public static final String URI = "URI"; + /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */ + public static final String LAST_TOKEN = "LAST_TOKEN"; + /** Constant for {@link HtmlSerializerContext#HTML_uriAnchorText} property. */ + public static final String URI_ANCHOR = "URI_ANCHOR"; + + + /** + * Look for URLs in {@link String Strings} ({@link Boolean}, default=<jk>true</jk>). + * <p> + * If a string looks like a URL (e.g. starts with <js>"http://"</js> or <js>"https://"</js>, then treat it like a URL + * and make it into a hyperlink based on the rules specified by {@link #HTML_uriAnchorText}. + */ + public static final String HTML_detectLinksInStrings = "HtmlSerializer.detectLinksInStrings"; + + /** + * Look for link labels in the <js>"label"</js> parameter of the URL ({@link Boolean}, default=<jk>true</jk>). + * <p> + * If the URL has a label parameter (e.g. <js>"?label=foobar"</js>), then use that as the anchor text of the link. + * <p> + * The parameter name can be changed via the {@link #HTML_labelParameter} property. + */ + public static final String HTML_lookForLabelParameters = "HtmlSerializer.lookForLabelParameters"; + + /** + * The parameter name to use when using {@link #HTML_lookForLabelParameters} ({@link String}, default=<js>"label"</js>). + */ + public static final String HTML_labelParameter = "HtmlSerializer.labelParameter"; + + final String uriAnchorText; + final boolean lookForLabelParameters, detectLinksInStrings; + final String labelParameter; + + /** + * Constructor. + * <p> + * Typically only called from {@link ContextFactory#getContext(Class)}. + * + * @param cf The factory that created this context. + */ + public HtmlSerializerContext(ContextFactory cf) { + super(cf); + uriAnchorText = cf.getProperty(HTML_uriAnchorText, String.class, TO_STRING); + lookForLabelParameters = cf.getProperty(HTML_lookForLabelParameters, Boolean.class, true); + detectLinksInStrings = cf.getProperty(HTML_detectLinksInStrings, Boolean.class, true); + labelParameter = cf.getProperty(HTML_labelParameter, String.class, "label"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/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); + } +}
