// // @(#)HTMLFormat.java 4/2002 // // Copyright 2002 Zachary DelProposto. All rights reserved. // Use is subject to license terms. // // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // Or from http://www.gnu.org/ // package dip.gui; import java.util.*; import java.text.*; /** * Formats HTML text (or, really, any text) similar to MessageFormat but with * the following features: * <pre> 1) pass a hashtable of key/value pairs keys MUST be String objects values can be any object that has a toString() method 2) markup input text as follows: Simple Insertion: ================ {name} = insert 'name', via the 'toString' method Formatted Insertion: =================== {date:name,<specifier>} {decimal:name,<specifier>} specifiers are optional; locale-specific formatting used. Loops: ===== {for:name @@name@ <== insert name[position] } Reserved words: "date:" "decimal:" "for:" Reserved Symbols: "{", "}" '@' (in for loops only) what about arrays? index directly {name:3} 3rd position of "name" {name:name2} position "name2" in name (name2 must be an Integer) or: 'for' loops for arrays {for:name blah blah blah HTML here @name (inserts name[i]) } keywords: for:name (uses array "name" for start/end) for:start:end conditions is always "<" @@index current position @@indexplus current position + 1 (for display purposes) @@variable@ @@variable@ date: // format-specifier: date decimal: // format-specifier: decimal </pre> */ public class HTMLFormat { // private static final String VAR_PREFIX = "@@"; // private Map map = null; private StringBuffer sb = null; // formatters private DecimalFormat decimalFormat = null; private SimpleDateFormat dateFormat = null; private String defaultDecimalPattern = null; private String defaultDatePattern = null; public static HTMLFormat getInstance() { return new HTMLFormat(); }// getInstance() public String format(String html, Map map) { this.map = map; sb.setLength(0); StringBuffer accum = new StringBuffer(1024); boolean inBrace = false; StringTokenizer st = new StringTokenizer(html,"{}",true); while(st.hasMoreTokens()) { String tok = st.nextToken(); if("{".equals(tok) && !inBrace) { inBrace = true; } else if("}".equals(tok) && inBrace) { inBrace = false; parseBetweenBraces(accum.toString()); accum.setLength(0); } else { if(inBrace) { accum.append(tok); } else { sb.append(tok); } } } return sb.toString(); }// format() private void parseBetweenBraces(String text) { // decision loop: check for keywords if(text.startsWith("decimal:")) { replaceDecimal(text); } else if(text.startsWith("date:")) { replaceDate(text); } else if(text.startsWith("for:")) { replaceFor(text); } else { // simple replace, unless we have a ':'; // then we need to see if we've got an array... int colonIndex = text.indexOf(':'); if(colonIndex != -1) { replaceArray(text, colonIndex); } else { replaceSimple(text); } } }// parseBetweenBraces() private void replaceSimple(String key) { Object lookup = map.get(key); sb.append(lookup.toString()); }// replaceSimple() private void replaceDate(String text) { int colonIdx = text.indexOf(':'); int commaIdx = text.indexOf(','); String key = null; String spec = null; if(commaIdx == -1) { key = text.substring(colonIdx+1); dateFormat.applyLocalizedPattern(defaultDatePattern); } else { key = text.substring(colonIdx+1, commaIdx); spec = text.substring(commaIdx+1); dateFormat.applyLocalizedPattern(spec); } Date date = (Date) map.get(key); sb.append(dateFormat.format(date)); }// replaceDate() private void replaceDecimal(String text) { int colonIdx = text.indexOf(':'); int commaIdx = text.indexOf(','); String key = null; String spec = null; if(commaIdx == -1) { key = text.substring(colonIdx+1); decimalFormat.applyLocalizedPattern(defaultDecimalPattern); } else { key = text.substring(colonIdx+1, commaIdx); spec = text.substring(commaIdx+1); decimalFormat.applyLocalizedPattern(spec); } Number num = (Number) map.get(key); sb.append(decimalFormat.format(num)); }// replaceDecimal() private void replaceArray(String text, int colonIdx) { String key = text.substring(0,colonIdx); String index = text.substring(colonIdx+1); Object[] objs = null; try { objs = (Object[]) map.get(key); } catch(ClassCastException e) { System.err.println("ERROR: HTMLFormat: value for key \""+key+"\" not an array."); return; } if(index.length() > 0) { sb.append( objs[parseOrLookupInt(index)] ); } else { System.err.println("ERROR: HTMLFormat: invalid index given for array value key \""+key+"\""); } }// replaceArray() private void replaceFor(String text) { // get 'for' parameters // for:name (uses array "name" for start/end) // for:start:end int start = 0; int end = 0; String tok0 = null; String tok1 = null; StringTokenizer st = new StringTokenizer(text.substring(0,text.indexOf(' ')), ":", false); st.nextToken(); // for: this has already been detected if(st.hasMoreTokens()) { tok0 = st.nextToken(); } else { System.err.println("HTMLFormat: for: loop without start/end parameters!"); return; } if(st.hasMoreTokens()) { tok1 = st.nextToken(); } // name based or index-based if(tok1 == null) { // only one token; name-based // derive start & end from this variable (which must be an array) try { Object[] objs = (Object[]) map.get(tok0); start = 0; end = objs.length; } catch(ClassCastException e) { System.err.println("ERROR: HTMLFormat: for: parameter \""+tok0+"\" is not an array!"); return; } } else { // two tokens; index-based (start:end) start = parseOrLookupInt(tok0); end = parseOrLookupInt(tok1); } // start of text block for loop; everything before this // is for::: statement; first space cuts this off. text = text.substring(text.indexOf(' ')); // create stringbuffer StringBuffer iterText = new StringBuffer(text.length() + 256); // iterate the loop; each time, go through and replace @@ variables // with the new values, and add this to the main string buffer. for(int i=start; i<end; i++) { // copy text iterText.setLength(0); iterText.append(text); // perform replacements replaceAll(iterText, "@@indexplus", String.valueOf(i+1)); replaceAll(iterText, "@@index", String.valueOf(i)); replaceAllVariables(iterText, i); // append sb.append(iterText); } }// replaceFor() private int parseInt(String text) { try { return Integer.parseInt(text); } catch(NumberFormatException e) { System.err.println("HTMLFormat: could not parse \""+text+"\" as Integer"); } return 0; }// parseInt() private int parseIntegerFromMap(String key) { Object obj = map.get(key); if(obj instanceof Integer) { try { return ((Integer)obj).intValue(); } catch(NumberFormatException e) { } } System.err.println("HTMLFormat: could not parse key \""+key+"\" as Integer"); return 0; }// parseIntegerFromMap() // if 'in' starts with a valid java identifier, look it up, and return the value // if 'in' starts with a digit, parse it, and return the value private int parseOrLookupInt(String in) { if(Character.isJavaIdentifierStart(in.charAt(0))) { return parseIntegerFromMap(in); } // index is a constant return parseInt(in); }// parseOrLookupInt // probably should be in Utils.java :: also used by OrderParser // private void replaceAll(StringBuffer in, String find, String replace) { int idx = 0; int start = in.indexOf(find, idx); while(start != -1) { int end = start + find.length(); in.replace(start, end, replace); // repeat search idx = start + replace.length(); start = in.indexOf(find, idx); } }// replacedAll() // looks for "@@" and if followed by any text, followed by "@"; // looks it up; if array, replace with indexed, otherwise, just print. // private void replaceAllVariables(StringBuffer in, int forIndex) { int idx = 0; int start = in.indexOf(VAR_PREFIX, idx); while(start != -1) { String replace = ""; int end = in.indexOf( "@", (start + VAR_PREFIX.length()) ); if(end - start > 48) { // probably not valid; skip continue; } if(end == -1) { break; } String key = in.substring(start+VAR_PREFIX.length(), end); Object obj = map.get(key); if(obj != null) { if(obj.getClass().isArray()) { Object[] array = (Object[]) obj; replace = array[forIndex].toString(); } else { replace = obj.toString(); } } in.replace(start, end+1, replace); // repeat search idx = start + replace.length(); start = in.indexOf(VAR_PREFIX, idx); } }// replaceAllVariables() protected HTMLFormat() { sb = new StringBuffer(8192); decimalFormat = new DecimalFormat(); dateFormat = new SimpleDateFormat(); defaultDecimalPattern = decimalFormat.toPattern(); defaultDatePattern = dateFormat.toPattern(); }// HTMLFormat() }// class HTMLFormat///