001: /*
002: #IFNDEF ALT_LICENSE
003: ThinWire(R) RIA Ajax Framework
004: Copyright (C) 2003-2007 Custom Credit Systems
005:
006: This library is free software; you can redistribute it and/or modify it under
007: the terms of the GNU Lesser General Public License as published by the Free
008: Software Foundation; either version 2.1 of the License, or (at your option) any
009: later version.
010:
011: This library is distributed in the hope that it will be useful, but WITHOUT ANY
012: WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
013: PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
014:
015: You should have received a copy of the GNU Lesser General Public License along
016: with this library; if not, write to the Free Software Foundation, Inc., 59
017: Temple Place, Suite 330, Boston, MA 02111-1307 USA
018:
019: Users who would rather have a commercial license, warranty or support should
020: contact the following company who invented, built and supports the technology:
021:
022: Custom Credit Systems, Richardson, TX 75081, USA.
023: email: info@thinwire.com ph: +1 (888) 644-6405
024: http://www.thinwire.com
025: #ENDIF
026: [ v1.2_RC2 ]
027: */
028: package thinwire.render.web;
029:
030: import java.io.ByteArrayInputStream;
031: import java.lang.reflect.Field;
032: import java.lang.reflect.InvocationTargetException;
033: import java.lang.reflect.Method;
034: import java.util.ArrayList;
035: import java.util.Collections;
036: import java.util.HashMap;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.logging.Level;
040: import java.util.logging.Logger;
041: import java.util.regex.Pattern;
042:
043: import javax.xml.parsers.SAXParser;
044: import javax.xml.parsers.SAXParserFactory;
045:
046: import org.xml.sax.Attributes;
047: import org.xml.sax.SAXException;
048: import org.xml.sax.helpers.AttributesImpl;
049: import org.xml.sax.helpers.DefaultHandler;
050:
051: import thinwire.ui.Application;
052: import thinwire.ui.style.Background;
053: import thinwire.ui.style.Border;
054: import thinwire.ui.style.Color;
055: import thinwire.ui.style.Font;
056:
057: /**
058: * @author Joshua J. Gertzen
059: * @author Ted C. Howard
060: */
061: class RichTextParser extends DefaultHandler {
062: private static final Logger log = Logger
063: .getLogger(RichTextParser.class.getName());
064: private static final Level LEVEL = Level.FINER;
065:
066: static final String STYLE_FONT_FAMILY = "ff";
067: static final String STYLE_COLOR = "fc";
068: static final String STYLE_FONT_SIZE = "fs";
069: static final String STYLE_FONT_WEIGHT = "fw";
070: static final String STYLE_TEXT_DECORATION = "fd";
071: static final String STYLE_FONT_STYLE = "ft";
072: static final String STYLE_BORDER_STYLE = "dt";
073: static final String STYLE_BORDER_WIDTH = "dw";
074: static final String STYLE_BORDER_COLOR = "dc";
075: static final String STYLE_BORDER_IMAGE = "di";
076: static final String STYLE_BACKGROUND_COLOR = "bc";
077: static final String STYLE_BACKGROUND_POSITION = "bp";
078: static final String STYLE_BACKGROUND_REPEAT = "br";
079: static final String STYLE_BACKGROUND_IMAGE = "bi";
080:
081: private static final String ATTR_HREF = "r";
082: private static final String ATTR_TARGET = "t";
083: private static final String ATTR_SRC = "s";
084: private static final String ATTR_WIDTH = "w";
085: private static final String ATTR_HEIGHT = "h";
086:
087: private static final Map<String, Tag> TAGS;
088: private static final Pattern TAG_REGEX;
089: static {
090: Map<String, Tag> tags = new HashMap<String, Tag>();
091: Map<String, Validator> map = new HashMap<String, Validator>();
092: map.put("family", new EnumValidator(Font.Family.class,
093: STYLE_FONT_FAMILY));
094: map.put("face", new EnumValidator(Font.Family.class,
095: STYLE_FONT_FAMILY));
096: map.put("color", new EnumValidator(Color.class, STYLE_COLOR));
097: map.put("size", new NumberValidator(STYLE_FONT_SIZE, 0, 128,
098: "pt"));
099: map.put("bold", new BooleanValidator(STYLE_FONT_WEIGHT, "bold",
100: "normal"));
101: map.put("underline", new BooleanValidator(
102: STYLE_TEXT_DECORATION, "underline", "none"));
103: map.put("strike", new BooleanValidator(STYLE_TEXT_DECORATION,
104: "line-through", "none"));
105: map.put("italic", new BooleanValidator(STYLE_FONT_STYLE,
106: "italic", "normal"));
107: tags.put("font", new Tag(map));
108: tags.put("b", new Tag(map, "bold", "true"));
109: tags.put("i", new Tag(map, "italic", "true"));
110: tags.put("u", new Tag(map, "underline", "true"));
111: tags.put("s", new Tag(map, "strike", "true"));
112:
113: map = new HashMap<String, Validator>();
114: map.put("edge", null);
115: tags.put("border", new Tag(newBorderSet("")));
116: tags.put("border left", new Tag(newBorderSet("Left"), map));
117: tags.put("border right", new Tag(newBorderSet("Right"), map));
118: tags.put("border top", new Tag(newBorderSet("Top"), map));
119: tags.put("border bottom", new Tag(newBorderSet("Bottom"), map));
120:
121: map = new HashMap<String, Validator>();
122: map.put("color", new EnumValidator(Color.class,
123: STYLE_BACKGROUND_COLOR));
124: map.put("position", new EnumValidator(
125: Background.Position.class, STYLE_BACKGROUND_POSITION));
126: map.put("repeat", new EnumValidator(Background.Repeat.class,
127: STYLE_BACKGROUND_REPEAT));
128: map.put("image", new URLValidator(STYLE_BACKGROUND_IMAGE));
129: tags.put("background", new Tag(map));
130:
131: map = new HashMap<String, Validator>();
132: map.put("href", new URLValidator(ATTR_HREF));
133: map.put("target", new TargetValidator(ATTR_TARGET));
134: tags.put("a", new Tag("a", map, true));
135:
136: map = new HashMap<String, Validator>();
137: map.put("src", new URLValidator(ATTR_SRC));
138: map.put("width", new NumberValidator(ATTR_WIDTH, 0,
139: Short.MAX_VALUE, null));
140: map.put("height", new NumberValidator(ATTR_HEIGHT, 0,
141: Short.MAX_VALUE, null));
142: tags.put("img", new Tag("img", map, false));
143:
144: tags.put("br", new Tag("br", null, false));
145:
146: StringBuilder sb = new StringBuilder();
147: sb.append(".*<(?:");
148:
149: for (String s : tags.keySet()) {
150: sb.append(s).append('|');
151: }
152:
153: sb.setCharAt(sb.length() - 1, ')');
154: sb.append(".*");
155: TAGS = tags;
156: TAG_REGEX = Pattern.compile(sb.toString());
157: }
158:
159: private static Map<String, Validator> newBorderSet(String edge) {
160: Map<String, Validator> map = new HashMap<String, Validator>();
161: String side = edge.length() == 0 ? "" : String.valueOf(edge
162: .charAt(0));
163: map.put("type", new EnumValidator(Border.Type.class,
164: STYLE_BORDER_STYLE + side));
165: map.put("size", new NumberValidator(STYLE_BORDER_WIDTH + side,
166: 0, 32, "px"));
167: map.put("color", new EnumValidator(Color.class,
168: STYLE_BORDER_COLOR + side));
169: return Collections.unmodifiableMap(map);
170: }
171:
172: private static class Tag {
173: String name = "span";
174: Map<String, Validator> attrMap;
175: Map<String, Validator> attrIgnoreMap;
176: String specificAttrName;
177: String specificAttrValue;
178: boolean children = true;
179:
180: Tag(String name, Map<String, Validator> attrMap,
181: boolean children) {
182: this .name = name;
183: this .attrMap = attrMap;
184: this .children = children;
185: }
186:
187: Tag(Map<String, Validator> attrMap) {
188: this .attrMap = attrMap;
189: }
190:
191: Tag(Map<String, Validator> attrMap,
192: Map<String, Validator> attrIgnoreMap) {
193: this .attrMap = attrMap;
194: this .attrIgnoreMap = attrIgnoreMap;
195: }
196:
197: Tag(Map<String, Validator> attrMap, String specificAttrName,
198: String specificAttrValue) {
199: this .attrMap = attrMap;
200: this .specificAttrName = specificAttrName;
201: this .specificAttrValue = specificAttrValue;
202: }
203: }
204:
205: private static class Depth {
206: List<Integer[]> tracker = new ArrayList<Integer[]>();
207: int size;
208:
209: void reset() {
210: tracker.clear();
211: size = 0;
212: }
213:
214: int add() {
215: if (size > 0)
216: tracker.get(size - 1)[0]++;
217: tracker.add(new Integer[] { 0, 0 });
218: ++size;
219: return size;
220: }
221:
222: void remove() {
223: tracker.remove(--size);
224: }
225:
226: int children() {
227: return tracker.get(size - 1)[0];
228: }
229:
230: void setMark(int mark) {
231: tracker.get(size - 1)[1] = mark;
232: }
233:
234: int getMark() {
235: return tracker.get(size - 1)[1];
236: }
237:
238: public String toString() {
239: return "Depth{size:" + size + ",children:" + children()
240: + ",mark:" + getMark() + "}";
241: }
242: }
243:
244: private static class Validator {
245: String jsPropName;
246:
247: Validator(String jsPropName) {
248: this .jsPropName = jsPropName;
249: }
250:
251: String getValue(String value) {
252: return value;
253: }
254: }
255:
256: private static class EnumValidator extends Validator {
257: Class c;
258:
259: EnumValidator(Class c, String jsPropName) {
260: super (jsPropName);
261: this .c = c;
262: }
263:
264: @Override
265: public String getValue(String value) {
266: String objValue = value;
267:
268: try {
269: Field f = c.getField(value.toUpperCase().replace('-',
270: '_'));
271: objValue = f.get(null).toString();
272: } catch (NoSuchFieldException e2) {
273: try {
274: Method m = c.getMethod("valueOf", String.class);
275: if (m.getReturnType() != c)
276: throw new NoSuchMethodException(
277: "public static " + c
278: + " valueOf(String value)");
279: objValue = m.invoke(null, objValue).toString();
280: } catch (NoSuchMethodException e) {
281: throw new RuntimeException(e);
282: } catch (InvocationTargetException e) {
283: throw new RuntimeException(e);
284: } catch (IllegalAccessException e) {
285: throw new RuntimeException(e);
286: }
287: } catch (IllegalAccessException e2) {
288: throw new RuntimeException(e2);
289: }
290:
291: return objValue;
292: }
293: }
294:
295: private static class BooleanValidator extends Validator {
296: String trueValueName;
297: String falseValueName;
298:
299: BooleanValidator(String jsPropName, String trueValueName,
300: String falseValueName) {
301: super (jsPropName);
302: this .trueValueName = trueValueName;
303: this .falseValueName = falseValueName;
304: }
305:
306: @Override
307: public String getValue(String value) {
308: return Boolean.valueOf(value) ? trueValueName
309: : falseValueName;
310: }
311: }
312:
313: private static class URLValidator extends Validator {
314:
315: URLValidator(String jsPropName) {
316: super (jsPropName);
317: }
318:
319: String getValue(String value, ComponentRenderer cr) {
320: return cr.getQualifiedURL(value);
321: }
322:
323: }
324:
325: private static class TargetValidator extends Validator {
326: TargetValidator(String jsPropName) {
327: super (jsPropName);
328: }
329:
330: @Override
331: public String getValue(String value) {
332: if (value.charAt(0) == '_')
333: throw new RuntimeException("value.charAt(0) == '_'");
334: return value;
335: }
336: }
337:
338: private static class NumberValidator extends Validator {
339: int min;
340: int max;
341: String units;
342:
343: NumberValidator(String jsPropName, int min, int max,
344: String units) {
345: super (jsPropName);
346: this .min = min;
347: this .max = max;
348: this .units = units;
349: }
350:
351: @Override
352: String getValue(String value) {
353: int size = Integer.parseInt(value);
354: if (size <= min || size > max)
355: throw new RuntimeException(jsPropName + " <= " + min
356: + " || " + jsPropName + " > " + max);
357: return units == null ? String.valueOf(size) : size + units;
358: }
359: }
360:
361: private static final Application.Local<SAXParser> INSTANCE = new Application.Local<SAXParser>() {
362: public SAXParser initialValue() {
363: try {
364: return SAXParserFactory.newInstance().newSAXParser();
365: } catch (Exception e) {
366: if (e instanceof RuntimeException)
367: throw (RuntimeException) e;
368: throw new RuntimeException(e);
369: }
370: }
371: };
372:
373: private SAXParser parser;
374: private Depth depth;
375: private ComponentRenderer renderer;
376: private StringBuilder sb;
377:
378: RichTextParser(ComponentRenderer cr) {
379: parser = INSTANCE.get();
380: depth = new Depth();
381: renderer = cr;
382: }
383:
384: Object parse(String richText) {
385: if (richText.indexOf('<') >= 0 && richText.indexOf('>') > 0
386: & TAG_REGEX.matcher(richText).matches()) {
387: try {
388: sb = new StringBuilder();
389: depth.reset();
390: parser.parse(new ByteArrayInputStream(("<richText>"
391: + richText + "</richText>").getBytes()), this );
392: if (log.isLoggable(LEVEL))
393: log.log(LEVEL, "RICH TEXT: " + sb.toString());
394: return sb;
395: } catch (Exception e) {
396: if (log.isLoggable(LEVEL))
397: log.log(LEVEL, "unable to parse rich text:"
398: + richText, e);
399: return richText;
400: }
401: } else {
402: return richText;
403: }
404: }
405:
406: @Override
407: public void startDocument() throws SAXException {
408: sb.append("[");
409: }
410:
411: @Override
412: public void startElement(String uri, String localName,
413: String qName, Attributes attr) throws SAXException {
414: int attrLen = attr.getLength();
415: int depthSize = depth.add();
416: if (log.isLoggable(LEVEL))
417: log.log(LEVEL, "depth=" + depth + ",uri=" + uri
418: + ",localName=" + localName + ",qName=" + qName
419: + ",attributes.getLength()=" + attrLen);
420:
421: if (qName.equals("richText")) {
422: if (depthSize > 1)
423: throw new SAXException("duplicate top-level element <"
424: + qName + ">");
425: } else {
426: if (depthSize == 1)
427: throw new SAXException(
428: "top-level element <richText> not found");
429: Tag tag = TAGS.get(qName);
430: if (tag == null)
431: throw new SAXException("invalid start of element <"
432: + qName + ">");
433:
434: if (tag.specificAttrName != null) {
435: if (attrLen != 0)
436: throw new SAXException("attributes specified for <"
437: + qName + ">");
438: attr = new AttributesImpl();
439: ((AttributesImpl) attr).addAttribute("",
440: tag.specificAttrName, tag.specificAttrName,
441: "CDATA", tag.specificAttrValue);
442: attrLen++;
443: }
444:
445: if (attrLen == 0 && tag.attrMap != null)
446: throw new SAXException("no attributes specified for <"
447: + qName + ">");
448: if (attrLen != 0 && tag.attrMap == null)
449: throw new SAXException("attributes specified for <"
450: + qName + ">");
451:
452: if (qName.equals("border") && attr.getValue("edge") != null) {
453: if (attrLen < 2)
454: throw new SAXException(
455: "no attributes specified for <" + qName
456: + " edge='" + attr.getValue("edge")
457: + "'>");
458: sb.append("{t:\"").append(tag.name).append("\",s:{");
459: String[] edges = attr.getValue("edge").split(",");
460:
461: for (String s : edges) {
462: tag = TAGS.get(qName + " " + s);
463: if (tag == null)
464: throw new SAXException(
465: "invalid <border edge=''> attribute '"
466: + s + "' specified");
467: writeAttributes(attr, tag.attrMap,
468: tag.attrIgnoreMap);
469: }
470:
471: //Close object
472: int len = sb.length();
473: sb.setCharAt(len - 1, '}');
474: sb.append(',');
475: depth.setMark(len + 1);
476: } else {
477: if (qName.equals("a")
478: && attr.getValue("target") == null) {
479: if (!(attr instanceof AttributesImpl))
480: attr = new AttributesImpl(attr);
481: ((AttributesImpl) attr)
482: .addAttribute(
483: "",
484: "target",
485: "target",
486: "CDATA",
487: "a"
488: + System
489: .identityHashCode(renderer));
490: }
491:
492: sb.append("{t:\"").append(tag.name).append("\",")
493: .append(tag.name.equals("span") ? "s" : "a")
494: .append(":{");
495: if (tag.attrMap != null)
496: writeAttributes(attr, tag.attrMap, null);
497: int index = sb.length() - 1;
498:
499: if (sb.charAt(index) == ',') {
500: sb.setCharAt(index, '}');
501: sb.append(',');
502: } else {
503: sb.append("},");
504: }
505:
506: depth.setMark(sb.length());
507: }
508: }
509: }
510:
511: @Override
512: public void characters(char[] ch, int start, int length)
513: throws SAXException {
514: depth.add();
515: if (log.isLoggable(LEVEL))
516: log.log(LEVEL, "depth=" + depth + ",characters="
517: + new String(ch, start, length));
518: sb.append("\"").append(ch, start, length).append("\",");
519: depth.remove();
520: }
521:
522: @Override
523: public void endElement(String uri, String localName, String qName)
524: throws SAXException {
525: if (log.isLoggable(LEVEL))
526: log.log(LEVEL, "depth=" + depth + ",uri=" + uri
527: + ",localName=" + localName + ",qName=" + qName);
528:
529: if (qName.equals("richText")) {
530: //if (depth.size > 1) throw new SAXException("duplicate top-level element <" + qName + ">");
531: } else {
532: Tag tag = TAGS.get(qName);
533: if (tag == null)
534: throw new SAXException("invalid end element </" + qName
535: + ">");
536: int children = depth.children();
537:
538: if (tag.children) {
539: //XXX is it an error if there were no children?
540: if (children > 0) {
541: if (children > 1) {
542: sb.insert(depth.getMark(), "c:[");
543: sb.setCharAt(sb.length() - 1, ']');
544: } else {
545: sb.insert(depth.getMark(), "c:");
546: }
547: } else {
548: //XXX Remove entire entry? Or is this an error?
549: }
550: } else if (children > 0) {
551: throw new SAXException("no children allowed for <"
552: + qName + "> tag");
553: }
554:
555: int index = sb.length() - 1;
556:
557: if (sb.charAt(index) == ',') {
558: sb.setCharAt(index, '}');
559: sb.append(',');
560: } else {
561: sb.append("},");
562: }
563: }
564:
565: depth.remove();
566: }
567:
568: @Override
569: public void endDocument() throws SAXException {
570: sb.setCharAt(sb.length() - 1, ']');
571: }
572:
573: private void writeAttributes(Attributes attributes,
574: Map<String, Validator> attMap,
575: Map<String, Validator> ignoreMap) throws SAXException {
576: for (int i = 0, cnt = attributes.getLength(); i < cnt; i++) {
577: String attrName = attributes.getQName(i);
578:
579: if (attMap.containsKey(attrName)) {
580: Validator v = attMap.get(attrName);
581: String value = attributes.getValue(i);
582:
583: if (v instanceof URLValidator) {
584: value = ((URLValidator) v)
585: .getValue(value, renderer);
586: } else {
587: value = v.getValue(value);
588: }
589:
590: sb.append(v.jsPropName).append(":");
591: sb.append("\"").append(value).append("\",");
592: } else if (ignoreMap == null
593: || !ignoreMap.containsKey(attrName)) {
594: throw new SAXException("invalid attribute '" + attrName
595: + "' specified");
596: }
597: }
598: }
599: }
|