package org.docx4j.model.fields; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart; import org.docx4j.wml.NumberFormat; /** * Formats the string value of the field according to the three possible formatting switches: * * + date-and-time-formatting-switch: \@ * + numeric-formatting-switch: \# * + general-formatting-switch: \* * * Note that the general-formatting-switch arguments CHARFORMAT and MERGEFORMAT are not handled here. * It is the responsibility of the calling code to handle these. * * @author alberto, jharrop * */ public class FormattingSwitchHelper { private static Logger log = LoggerFactory.getLogger(FormattingSwitchHelper.class); /* http://office.microsoft.com/en-us/word-help/insert-and-format-field-codes-in-word-2010-HA101830917.aspx * Switches: * * Format switches (\*): * Capitalization formats * \* Caps Capitalizes the first letter of each word. * \* FirstCap Capitalizes the first letter of the first word. * \* Upper Capitalizes all letters. * \* Lower Capitalizes none of the result; all letters are lowercase. * * Number formats * \* alphabetic Lowercase alphabetic characters (a, b, c). * \* ALPHABETIC Uppercase alphabetic characters (A, B, C). * \* Arabic Arabic cardinal numerals (1, 2, 3). * \* ArabicDash Only PAGE! Arabic cardinal numerals surrounded by hyphen characters (- 1 -, - 2 -, - 3 -). * \* CardText Lowercase Cardinal text (can be combined with capitalization formats). (one, two, three). * \* DollarText ........ For example, { = 9.20 + 5.35 \* DollarText \* Upper } displays FOURTEEN AND 55/100. * \* Hex Hexadecimal numbers. * \* OrdText Lowercase Ordinal text (can be combined with capitalization formats). (first, second, third) * \* Ordinal Ordinal Arabic numerals (1st, 2nd, 3rd). * \* roman Lowercase Roman numerals (i, ii, iii). * \* ROMAN Uppercase Roman numerals (I, II, III). * * Character formats and protecting previously applied formats * \* Charformat This switch applies the formatting of the first letter of the field name to the entire result. * \* MERGEFORMAT This switch applies the formatting of the previous result to the new result. * * Numeric format switch (\#): * * 0 (zero) This format item specifies the requisite numeric places to display in the result. If the result does not include a digit in that place, Word displays a 0 (zero). * # This format item specifies the requisite numeric places to display in the result. If the result does not include a digit in that place, Word displays a space. * x This format item drops digits to the left of the "x" placeholder. If the placeholder is to the right of the decimal point, Word rounds the result to that place. * . (decimal point) This format item determines the decimal point position. (Use the decimal symbol that is specified as part of the regional settings in Control Panel.) * , (digit grouping symbol) This format item separates a series of three digits. (Use the digit grouping symbol that is specified as part of the regional settings in Control Panel.) * - (minus sign) This format item adds a minus sign to a negative result or adds a space if the result is positive or 0 (zero). * + (plus sign) This format item adds a plus sign to a positive result, a minus sign to a negative result, or a space if the result is 0 (zero). * %, $, *, and so on This format item includes the specified character in the result. * positive; negative[; zero] This format item specifies different number formats for a positive result, a negative result, and a 0 (zero) result, separated by semicolons. * `numbereditem` This format item displays the number of the preceding item that you numbered by using the Caption command (References tab, Captions group) or by inserting a SEQ field. * * * * Date Time format switch (\@): * Quotation marks are not required around simple date-time formats that do not include spaces or text * * Month * M Number without a leading 0 (zero) for single-digit months. * MM Number with a leading 0 (zero) for single-digit months. * MMM Three-letter abbreviation. * MMMM Full name. * * Day * d Day of the month as a number without a leading 0 (zero) for single-digit days. * dd Day of the month as a number with a leading 0 (zero) for single-digit days. * ddd Day of the week as three-letter abbreviation. * dddd Day of the week as its full name. * * Year * yy Year as two digits with a leading 0 (zero) for years 01 through 09. * yyyy Year as four digits. * * Hours * h (12h) Hour without a leading 0 (zero) for single-digit hours. * H (24h) Hour without a leading 0 (zero) for single-digit hours. * hh (12h) Hour with a leading 0 (zero) for single-digit hours. * HH (24h) Hour with a leading 0 (zero) for single-digit hours. * * Minutes * m Minutes without a leading 0 (zero) for single-digit minutes. * mm Minutes with a leading 0 (zero) for single-digit minutes. * * Seconds * s Seconds without a leading 0 (zero) for single-digit seconds. * ss Seconds with a leading 0 (zero) for single-digit seconds. * * AM/PM * am/pm or AM/PM Displays AM or PM as uppercase. * * Other Date Time items * 'text' Display any specified text in a date or time. Enclose the text in single quotation marks. * (How do you escape a single quote? - The example of the page above causes an error in word...) * character This format item includes the specified character in a date or time, such as a : (colon), - (hyphen), * (asterisk), or space. * `numbereditem` Number of the preceding item that you numbered. * */ protected static final ThreadLocal<Map<String, SimpleDateFormat>> DATE_FORMATS = new ThreadLocal<Map<String, SimpleDateFormat>>(){ protected Map<String, SimpleDateFormat> initialValue() { HashMap<String, SimpleDateFormat> hashMap = new HashMap<String, SimpleDateFormat>(); hashMap.put(null, (SimpleDateFormat)SimpleDateFormat.getDateTimeInstance()); return hashMap; } }; /** Conversion of page number formats to fo as defined * <ul> * <li>in w:fldSimple</li> * <li>in w:pgNumType w:fmt</li> * </ul> */ protected static final String DEFAULT_FORMAT_PAGE_TO_FO = "1"; //A marker for the NumberFormat.NONE - value protected static final String NONE_STRING = new String(); protected static final Map<String, String> FORMAT_PAGE_TO_FO = new HashMap<String, String>(); public static final int DECORATION_NONE = 0; public static final int DECORATION_DASH = 1; protected static final String MERGEFORMAT = "MERGEFORMAT"; // constants protected static final String FO_PAGENUMBER_DECIMAL = "1"; // '1' protected static final String FO_PAGENUMBER_LOWERALPHA = "a"; // 'a' protected static final String FO_PAGENUMBER_UPPERALPHA = "A"; // 'A' protected static final String FO_PAGENUMBER_LOWERROMAN = "i"; // 'i' protected static final String FO_PAGENUMBER_UPPERROMAN = "I"; // 'I' protected static final Map<String, String> DATE_FORMAT_ITEMS_TO_JAVA = new HashMap<String, String>(); static { FORMAT_PAGE_TO_FO.put("Arabic", FO_PAGENUMBER_DECIMAL); FORMAT_PAGE_TO_FO.put("ArabicDash", FO_PAGENUMBER_DECIMAL); FORMAT_PAGE_TO_FO.put("alphabetic", FO_PAGENUMBER_LOWERALPHA); FORMAT_PAGE_TO_FO.put("ALPHABETIC", FO_PAGENUMBER_UPPERALPHA); FORMAT_PAGE_TO_FO.put("roman", FO_PAGENUMBER_LOWERROMAN); FORMAT_PAGE_TO_FO.put("ROMAN", FO_PAGENUMBER_UPPERROMAN); FORMAT_PAGE_TO_FO.put(NumberFormat.NONE.value(), NONE_STRING); //"none" FORMAT_PAGE_TO_FO.put(NumberFormat.DECIMAL.value(), FO_PAGENUMBER_DECIMAL); // "decimal" FORMAT_PAGE_TO_FO.put(NumberFormat.NUMBER_IN_DASH.value(), FO_PAGENUMBER_DECIMAL); //"numberInDash" FORMAT_PAGE_TO_FO.put(NumberFormat.LOWER_LETTER.value(), FO_PAGENUMBER_LOWERALPHA); //"lowerLetter" FORMAT_PAGE_TO_FO.put(NumberFormat.UPPER_LETTER.value(), FO_PAGENUMBER_UPPERALPHA); //"upperLetter" FORMAT_PAGE_TO_FO.put(NumberFormat.LOWER_ROMAN.value(), FO_PAGENUMBER_LOWERROMAN); //"lowerRoman" FORMAT_PAGE_TO_FO.put(NumberFormat.UPPER_ROMAN.value(), FO_PAGENUMBER_UPPERROMAN); //"upperRoman" //Month DATE_FORMAT_ITEMS_TO_JAVA.put("M","M"); //Number without a leading 0 (zero) for single-digit months. DATE_FORMAT_ITEMS_TO_JAVA.put("MM","MM"); //Number with a leading 0 (zero) for single-digit months. DATE_FORMAT_ITEMS_TO_JAVA.put("MMM","MMM"); //Three-letter abbreviation. DATE_FORMAT_ITEMS_TO_JAVA.put("MMMM","MMMM"); //Full name. //Day DATE_FORMAT_ITEMS_TO_JAVA.put("d","d"); //Day of the month as a number without a leading 0 (zero) for single-digit days. DATE_FORMAT_ITEMS_TO_JAVA.put("dd","dd"); //Day of the month as a number with a leading 0 (zero) for single-digit days. DATE_FORMAT_ITEMS_TO_JAVA.put("ddd","EEE"); //Day of the week as three-letter abbreviation. DATE_FORMAT_ITEMS_TO_JAVA.put("dddd","EEEE"); //Day of the week as its full name. //Year DATE_FORMAT_ITEMS_TO_JAVA.put("yy","yy"); //Year as two digits with a leading 0 (zero) for years 01 through 09. DATE_FORMAT_ITEMS_TO_JAVA.put("yyyy","yyyy"); //Year as four digits. //Hour DATE_FORMAT_ITEMS_TO_JAVA.put("h","K"); //(12h) Hour without a leading 0 (zero) for single-digit hours. DATE_FORMAT_ITEMS_TO_JAVA.put("H","H"); //(24h) Hour without a leading 0 (zero) for single-digit hours. DATE_FORMAT_ITEMS_TO_JAVA.put("hh","KK"); //(12h) Hour with a leading 0 (zero) for single-digit hours. DATE_FORMAT_ITEMS_TO_JAVA.put("HH","HH"); //(24h) Hour with a leading 0 (zero) for single-digit hours. //Minute DATE_FORMAT_ITEMS_TO_JAVA.put("m","m"); //Minutes without a leading 0 (zero) for single-digit minutes. DATE_FORMAT_ITEMS_TO_JAVA.put("mm","mm"); //Minutes with a leading 0 (zero) for single-digit minutes. //Second DATE_FORMAT_ITEMS_TO_JAVA.put("s","s"); //Seconds without a leading 0 (zero) for single-digit seconds. DATE_FORMAT_ITEMS_TO_JAVA.put("ss","ss"); //Seconds with a leading 0 (zero) for single-digit seconds. //AM/PM //DATE_FORMAT_ITEMS_TO_JAVA.put("am/pm","a"); //Displays AM or PM as uppercase. //DATE_FORMAT_ITEMS_TO_JAVA.put("AM/PM","a"); //Displays AM or PM as uppercase. } private static class FieldResultIsNotADateOrTimeException extends Exception { public FieldResultIsNotADateOrTimeException(){ super(); } public FieldResultIsNotADateOrTimeException(String string) { super(string); } } private static class FieldResultIsNotANumberException extends Exception { public FieldResultIsNotANumberException(){ super(); } public FieldResultIsNotANumberException(String string) { super(string); } } public static String applyFormattingSwitch(WordprocessingMLPackage wmlPackage, FldSimpleModel model, String value) throws Docx4JException { return applyFormattingSwitch(wmlPackage, model, value, null); } public static String applyFormattingSwitch(WordprocessingMLPackage wmlPackage, FldSimpleModel model, String value, String lang) throws Docx4JException { // date-and-time-formatting-switch: \@ Date date = null; try { String dtFormat = findFirstSwitchValue("\\@", model.getFldParameters(), true); if (dtFormat==null) { // SPECIFICATION: If no date-and-time-formatting-switch is present, // a date or time result is formatted in an implementation-defined manner. // TODO .. analyse what Word does // for now, just leave as is } else if (dtFormat.equals("")) { // Verified with Word 2010 sp1 for DOCPROPERTY (very likely others are the same) return "Error! Switch argument not specified."; } else { log.debug("Applying date format " + dtFormat + " to " + value); date = getDate(model, value ); // For \@ date formatting, Word seems to give same results for // DATE, DOCPROPERTY, and MERGEFIELD, // but to have a different code path for = // OK, its a date if ( model.fldName.equals("=")) { // For =, today's date is used! date = new Date(); } value = formatDate(model, dtFormat, date, lang); } } catch (FieldResultIsNotADateOrTimeException e) { // SPECIFICATION: If the result of a field is not a date or time, // the date-and-time-formatting-switch has no effect } // numeric-formatting-switch: \# double number=-1; if (date==null) { // It is not a date, so see whether it is a number String nFormat = findFirstSwitchValue("\\#", model.getFldParameters(), true); if (nFormat!=null) { if (nFormat.equals("") ) { return "Error! Switch argument not specified."; // Verified with Word 2010 sp1, for =, DOCPROPERTY, MERGEFIELD // TODO unless it looks like a bookmark, eg {=AA \#} ? } else { try { number = getNumber(wmlPackage, model, value); } catch (FieldResultIsNotANumberException e) { // Is value a bookmark? // If so TODO set value to bookmark contents //Otherwise log.debug(e.getMessage()); // Word 2010 produces something like: "!Syntax Error" return "!Syntax Error"; } value = formatNumber(model, nFormat, number, lang); // SPECIFICATION: If no numeric-formatting-switch is present, // a numeric result is formatted without leading spaces or // trailing fractional zeros. // If the result is negative, a leading minus sign is present. // If the result is a whole number, no radix point is present. // We'll only honour this if the number is really a number. // Not, for example, CL.87559-p // try { // number = Double.parseDouble(value); // value = formatNumber(model, "#.##########", number ); // } catch (Exception e) { // log.debug(value + " is not a number"); // } // That's commented out, because in reality (Word 2010 sp1), // if there is no '\#', it is not treated as a number } } } // general-formatting-switch: \* // SPECIFICATION: A general-formatting-switch specifies a variety of // formats for a numeric or text result. If the result type of a field // does not correspond to the format specified, this switch has no effect. // Word 2010 can handle: // \@ "d 'de' MMMM 'de' yyyy " \* Upper" // (ie \@ and \* at same time) // but \@ must be before \* String gFormat = findFirstSwitchValue("\\*", model.getFldParameters(), false); value = gFormat==null ? value : formatGeneral(model, gFormat, value ); log.debug("Result -> " + value + "\n"); return value; } private static Date getDate(FldSimpleModel model, String dateStr) throws FieldResultIsNotADateOrTimeException { // eg 31/3/2013 // Word's default format for date: 7/04/2013 // and time: 11:05 AM /* Word seems happy to parse dates in any of the following formats: * <vt:lpwstr>13/4/2013</vt:lpwstr> <vt:lpwstr>13/04/2013</vt:lpwstr> <vt:lpwstr>13/04/13</vt:lpwstr> <vt:lpwstr>13-04-2013</vt:lpwstr> <vt:lpwstr>13/04/2013 11:05 AM</vt:lpwstr> <vt:lpwstr>2013-08-19T00:00:00Z</vt:lpwstr> * * Control Panel > Region & Language > Formats determines whether Word 2010 interprets 11/12 * as 11 Dec, or 12 Nov. * * But we'll use a docx4j property to control this. */ // parse the string into a date. String inputFormat = DateFormatInferencer.determineDateFormat(dateStr); if (inputFormat==null) { log.debug("Unrecognised format; Can't parse " + dateStr); return null; } else { log.debug("Parsing with format: " + inputFormat); Date date = null; try { DateFormat dateTimeFormat = new SimpleDateFormat(inputFormat); // is this threadsafe in static method? return (Date)dateTimeFormat.parse(dateStr); } catch (ParseException e) { log.warn("Can't parse " + dateStr + " using format " + inputFormat); throw new FieldResultIsNotADateOrTimeException(); } } } private static double getNumber(WordprocessingMLPackage wmlPackage, FldSimpleModel model, String value) throws FieldResultIsNotANumberException { WordprocessingMLPackage pkg = wmlPackage; String decimalSymbol=null; if (pkg!=null && pkg.getMainDocumentPart().getDocumentSettingsPart()!=null) { DocumentSettingsPart docSettingsPart = pkg.getMainDocumentPart().getDocumentSettingsPart(); if (docSettingsPart.getJaxbElement().getDecimalSymbol()!=null) { decimalSymbol = docSettingsPart.getJaxbElement().getDecimalSymbol().getVal(); } } // For DOCPROPERTY field, and possibly some others, but not "=", // Word will parse "€180,000.00 EUR" as a number if (model.fldName.equals("DOCPROPERTY") || model.fldName.equals("MERGEFIELD")) { // First, parse the value NumberExtractor nex = new NumberExtractor(decimalSymbol); try { value = nex.extractNumber(value); } catch (java.lang.IllegalStateException noMatch) { // There is no number in this string. // In this case Word just inserts the non-numeric text, // without attempting to format the number throw new FieldResultIsNotANumberException("No number in " + value); } } try { return Double.parseDouble(value); } catch (Exception e) { // TODO: is it a bookmark? throw new FieldResultIsNotANumberException(); } } private static String formatNumber( FldSimpleModel model, String wordNumberPattern, double dub, String lang) throws FieldFormattingException { // OK, now we have a number, let's apply the formatting string /* Per the spec, if no numeric-formatting-switch is present, a numeric result is formatted * without leading spaces or trailing fractional zeros. * If the result is negative, a leading minus sign is present. * If the result is a whole number, no radix point is present. */ // boolean encounteredPlus = false; // boolean encounteredMinus = false; boolean encounteredDecimalPoint = false; boolean encounteredX = false; // in Java's NumberFormatter, you can't mix # and 0, // but you can swap to the other after the decimal point char fillerBeforeDecimalPoint= '\0'; char fillerAfterDecimalPoint= '\0'; // in Java's NumberFormatter, you have # or 0, then a literal or %,$ etc then # or 0 again boolean encounteredNonFiller =false; int round = 0; int fillerBeforeDecimalPointCount = 0; // to check for an anomolous result StringBuilder buffer = new StringBuilder(32); int valueStart = -1; int idx = 0; int idx2 = 0; char ch = '\0'; char lastCh = '\0'; boolean inLiteral = false; if ((wordNumberPattern == null) || (wordNumberPattern.length() == 0)) { wordNumberPattern = "#.##########"; } if ((wordNumberPattern != null) && (wordNumberPattern.length() > 0)) { while (idx < wordNumberPattern.length()) { ch = wordNumberPattern.charAt(idx); if (ch == '\'') { if (inLiteral) { // End literal buffer.append(wordNumberPattern.substring(valueStart, idx)); //ignore closing ' idx2 = idx + 1; //skip ' /* * Word treats the whitespace outside the right single quote as significant, * and inserts zero, one or many spaces as if the whitespace was inside the * literal. * * WARNING: downstream XML processing needs to treat * this whitespace as significant. */ while ((idx2 < wordNumberPattern.length()) && (wordNumberPattern.charAt(idx2) == ' ')) { buffer.append(' '); idx2++; } buffer.append('\''); idx = idx2 - 1; inLiteral = false; valueStart = -1; } else { // Start literal if (valueStart > -1) { appendNumberItem(buffer, wordNumberPattern.substring(valueStart, idx)); } inLiteral = true; valueStart = idx; if (fillerBeforeDecimalPoint != '\0' || fillerAfterDecimalPoint != '\0') encounteredNonFiller = true; } } else if (!inLiteral) { // if (lastCh != ch) { // if (valueStart > -1) { // appendNumberItem(buffer, wordNumberPattern.substring(valueStart, idx)); // } // valueStart = idx; // lastCh = ch; // } if (ch=='x') { //wordNumberPattern.replaceFirst("x", "#"); if (encounteredDecimalPoint) { encounteredX = true; round++; // we honour the last if (fillerAfterDecimalPoint == '\0') { buffer.append('#'); // replacing x fillerAfterDecimalPoint ='#'; // and now we're committed to that } else { buffer.append(fillerAfterDecimalPoint); } } else { throw new FieldFormattingException("TODO implement 'x' digit dropper before decimal point "); } } else { if ((ch=='0')||(ch=='#')) { if (encounteredNonFiller) { throw new FieldFormattingException("Can't format arbitrary character between [0|#]* "); } if (encounteredDecimalPoint) { round++; // Use uniform filler char if (fillerAfterDecimalPoint == '\0') { fillerAfterDecimalPoint=ch; } buffer.append(fillerAfterDecimalPoint); } else { if (fillerBeforeDecimalPoint == '\0') { fillerBeforeDecimalPoint=ch; } buffer.append(fillerBeforeDecimalPoint); fillerBeforeDecimalPointCount++; } } else if ((ch=='%') // Java special characters ||(ch=='\u2030') // ‰ ||(ch=='E') ||(ch=='\u00A4') // ¤ ) { // escape them by quoting buffer.append('\''); buffer.append(ch); buffer.append('\''); if (fillerBeforeDecimalPoint != '\0' || fillerAfterDecimalPoint != '\0') encounteredNonFiller = true; } else if (ch=='+') { // Drop it // encounteredPlus = true; } else if (ch=='-') { // Drop it // encounteredMinus = true; } else { buffer.append(ch); if (ch=='.') { encounteredDecimalPoint = true; } else if (ch==',' || ch==' ') { // ok } else { if (fillerBeforeDecimalPoint != '\0' || fillerAfterDecimalPoint != '\0') encounteredNonFiller = true; } } } } idx++; } } if (valueStart > -1) { appendDateItem(buffer, wordNumberPattern.substring(valueStart)); } if (fillerBeforeDecimalPointCount<1 && (wordNumberPattern != null) && (wordNumberPattern.length() > 0)) { /* * Word loses the negative sign! * =-0.75 \# .###x WORD: .75 =-.75 \# .###x WORD: .75 =-0.75 \# .### WORD: .75 =-.75 \# .### WORD: .75 =-0.75 \# .000 WORD: .750 =-.75 \# .000 WORD: .750 * Word returns the fractional part only! =95.4 \# .00 WORD: .40 =95.4 \# .## WORD: .4 =95.4 \# $###.00 OK */ if ( (dub<0) // Word loses the negative sign! || (dub>=1) ) // Word returns the fractional part only! throw new FieldFormattingException("Refusing to replicate Word anomolous result. "); } String javaFormatter = buffer.toString(); DecimalFormat formatter = null; try { if(lang != null){ formatter = new DecimalFormat(javaFormatter, new DecimalFormatSymbols(localeforLanguageTag(lang))); // lang is eg "fr-CA" IETF BCP 47 tag } else { formatter = new DecimalFormat(javaFormatter); } } catch (java.lang.IllegalArgumentException iae) { // Malformed pattern throw new FieldFormattingException(iae.getMessage() + " from " + wordNumberPattern); } if (encounteredX) { formatter.setMaximumFractionDigits(round); } return formatter.format(dub); } /** * Substitute for Java 7's Locale.forLanguageTag * * @param locale * @return */ private static Locale localeforLanguageTag(String locale) { // Adapted from http://stackoverflow.com/a/15238594/1031689 log.debug(locale + " to Locale"); String parts[] = locale.split("-", -1); if (parts.length == 1) { return new Locale(parts[0]); } else if (parts.length == 2 // || (parts.length == 3 && parts[2].startsWith("#")) ) { return new Locale(/* language */parts[0], /* country */parts[1]); } else { return new Locale(parts[0], parts[1], parts[2]); } } private static void appendNumberItem(StringBuilder buffer, String dateItem) { // identity for now buffer.append(dateItem); } private static String formatGeneral( FldSimpleModel model, String format, String value) { // Note that the general-formatting-switch arguments CHARFORMAT and MERGEFORMAT // are not handled here. It is the responsibility of the calling code // to handle these. if ( format.equals("")) { return "Error! Switch argument not specified."; } // // so: // if ( format.equals("")) { // return value; // } // TODO: handle the SMALLCAPS exception! log.debug("Applying general format " + format + " to " + value); if (format.toUpperCase().contains("CAPS") ) { String[] bits = value.split(" "); StringBuffer sb = new StringBuffer(); for (int i= 0; i<bits.length; i++) { // System.out.println("'" + bits[i] + "'"); if (i>0) sb.append(" "); sb.append(firstCap(bits[i])); // TODO: test what Word does with whitespace } return sb.toString(); } if (format.toUpperCase().contains("FIRSTCAP") ) { return firstCap(value); } if (format.toUpperCase().contains("UPPER") ) { return value.toUpperCase(); } if (format.toUpperCase().contains("LOWER") ) { return value.toLowerCase(); } log.debug("Ignoring format: " + format); // This method does not currently handle: // alphabetic, arabic, cardtext, dollartext, hex, ordtext, ordinal, or roman // so throw unimplemented, else // mimic Word by including value "Error! Unknown switch argument" return value; } private static String firstCap(String value) { if (value == null || value.length()==0) { return ""; } else if (value.length()==1) { return value.substring(0, 1).toUpperCase(); } else if (value.startsWith("\"")) { return "\"" + value.substring(1, 2).toUpperCase() + value.substring(2).toLowerCase(); } else { // (value.length()>1) return value.substring(0, 1).toUpperCase() + value.substring(1).toLowerCase(); } } /** Conversion of the word page number format to the fo page number format. * * @param wordName word page number format * @return null if the wordName is null, the correspondig fo value if present or a default. */ public static String getFoPageNumberFormat(String wordName) { String ret = null; if ((wordName != null) && (wordName.length() > 0)) { ret = FORMAT_PAGE_TO_FO.get(wordName); if (ret == null) { ret = DEFAULT_FORMAT_PAGE_TO_FO; } else if (ret == NONE_STRING) { ret = null; } } return ret; } /** Check if the page number format has a decoration (eg. dash). * * @param wordName word page number format * @return decoration type (one of the DECORATION_xxx constants). */ public static int getFoPageNumberDecoration(String wordName) { int ret = DECORATION_NONE; if ((wordName != null) && (wordName.length() > 0)) { if ("ArabicDash".equals(wordName) || NumberFormat.NUMBER_IN_DASH.value().equals(wordName)) { ret = DECORATION_DASH; } } return ret; } public static String getFldSimpleName(String instr) { String ret = null; int startValue = 0; int endValue = -1; if ((instr != null) && (instr.length() > 0)) { while ((startValue < instr.length()) && (instr.charAt(startValue) == ' ')) startValue++; endValue = startValue; while ((endValue < instr.length()) && (instr.charAt(endValue) != ' ')) endValue++; if (startValue < instr.length()) { ret = instr.substring(startValue, endValue).toUpperCase(); } } return ret; } public static String convertDatePattern(String wordDatePattern) { StringBuilder buffer = new StringBuilder(32); int valueStart = -1; int idx = 0; int idx2 = 0; char ch = '\0'; char lastCh = '\0'; boolean inLiteral = false; if ((wordDatePattern != null) && (wordDatePattern.length() > 0)) { while (idx < wordDatePattern.length()) { ch = wordDatePattern.charAt(idx); if (ch == '\'') { if (inLiteral) { buffer.append(wordDatePattern.substring(valueStart, idx)); //ignore closing ' idx2 = idx + 1; //skip ' while ((idx2 < wordDatePattern.length()) && (wordDatePattern.charAt(idx2) == ' ')) { buffer.append(' '); idx2++; } buffer.append('\''); idx = idx2 - 1; inLiteral = false; valueStart = -1; } else { if (valueStart > -1) { appendDateItem(buffer, wordDatePattern.substring(valueStart, idx)); } inLiteral = true; valueStart = idx; } } else if (!inLiteral) { if (lastCh != ch) { if (valueStart > -1) { appendDateItem(buffer, wordDatePattern.substring(valueStart, idx)); } valueStart = idx; lastCh = ch; } if (((ch=='a') || (ch=='A')) && (isAMPM(wordDatePattern, idx))) { buffer.append('a'); idx += 4; lastCh='\0'; valueStart = -1; } } idx++; } } if (valueStart > -1) { appendDateItem(buffer, wordDatePattern.substring(valueStart)); } return buffer.toString(); } private static boolean isAMPM(String wordDatePattern, int idx) { int i=idx; //The a(A) of am/pm (AM/PM) doesn't need to be checked, //it was the reason why this got called. return ( (idx + 4 < wordDatePattern.length()) && ((wordDatePattern.charAt(++i) == 'm') || (wordDatePattern.charAt(i) == 'M')) && ( wordDatePattern.charAt(++i) == '/') && ((wordDatePattern.charAt(++i) == 'p') || (wordDatePattern.charAt(i) == 'P')) && ((wordDatePattern.charAt(++i) == 'm') || (wordDatePattern.charAt(i) == 'M')) ); } private static void appendDateItem(StringBuilder buffer, String dateItem) { String javaVal = DATE_FORMAT_ITEMS_TO_JAVA.get(dateItem); buffer.append(javaVal != null ? javaVal : dateItem); } public static String formatDate(FldSimpleModel model) { return formatDate(model, new Date()); } public static String formatDate(FldSimpleModel model, Date date) { String format = findFirstSwitchValue("\\@", model.getFldParameters(), true); return formatDate(model, format, date ); } public static String formatDate(FldSimpleModel model, String format, Date date) { return formatDate(model, format, date, null); } private static String formatDate(FldSimpleModel model, String format, Date date, String lang) { DateFormat dateFormat = null; if ((format != null) && (format.length() > 0)) { SimpleDateFormat simpleDateFormat = getSimpleDateFormat(lang); simpleDateFormat.applyPattern(convertDatePattern(format)); dateFormat = simpleDateFormat; } else { dateFormat = (model.getFldName().indexOf("DATE") > -1 ? SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT) : SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT)); } return dateFormat.format(date); } public static boolean hasSwitch(String switchDef, List<String> fldParameters) { return (findSwitch(switchDef, 0, fldParameters) > -1); } public static String findFirstSwitchValue(String switchDef, List<String> fldParameters, boolean ignoreMergeformat) { int pos = findSwitch(switchDef, 0, fldParameters); String switchValue = null; if (pos > -1) { switchValue = getSwitchValue(pos + 1, fldParameters); if (MERGEFORMAT.equals(switchValue) && ignoreMergeformat) { switchValue = null; pos = findSwitch(switchDef, pos + 1, fldParameters); if (pos > -1) { switchValue = getSwitchValue(pos + 1, fldParameters); } } // switch was found, so return empty value switchValue = switchValue==null ? "" : switchValue; } return switchValue; } public static String getSwitchValue(int pos, List<String> fldParameters) { String ret = null; if ((fldParameters != null) && (pos > -1) && (pos < fldParameters.size())) { ret = fldParameters.get(pos); if ((ret.length() > 1) && (ret.charAt(0) == '"') && (ret.charAt(ret.length() - 1) == '"')) { ret = ret.substring(1, ret.length() - 1); } } if ((ret != null) && (ret.length() > 0) && (ret.charAt(0) == '\\')) { //don't return a switch as a switch value; ret = null; } return ret; } public static List<String> findAllSwitchValues(String switchDef, List<String> fldParameters) { List<String> ret = null; int pos = 0; while ((pos = findSwitch(switchDef, pos, fldParameters)) > -1) { if (ret == null) { ret = new ArrayList<String>(); } pos++; ret.add(getSwitchValue(pos, fldParameters)); } return ret; } public static int findSwitch(String switchDef, int startPos, List<String> fldParameters) { int ret = -1; if ((fldParameters != null) && (!fldParameters.isEmpty())) { for (int i=startPos; i<fldParameters.size(); i++) { if (switchDef.equals(fldParameters.get(i))) { return i; } } } return -1; } /** * @param language abbreviation like "fr-CA", * @return SimpleDateFormat instance for specified language * if null will @return SimpleDateFormat.getDateTimeInstance() */ private static SimpleDateFormat getSimpleDateFormat(String lang) { Map<String, SimpleDateFormat> dateFormatsMap = DATE_FORMATS.get(); SimpleDateFormat dateFormat = dateFormatsMap.get(lang); if (dateFormat == null) { // dateFormat = (SimpleDateFormat)SimpleDateFormat.getDateTimeInstance(DateFormat.DEFAULT, 0, Locale.forLanguageTag(lang)); // Be Java 6 compatible dateFormat = (SimpleDateFormat)SimpleDateFormat.getDateTimeInstance(DateFormat.DEFAULT, 0, localeforLanguageTag(lang)); dateFormatsMap.put(lang, dateFormat); } return dateFormat; } }