/* * Freeplane - mind map editor * Copyright (C) 2011 Volker Boerchers * * This file author is Volker Boerchers * * 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, see <http://www.gnu.org/licenses/>. */ package org.freeplane.features.format; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.Format; 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.Properties; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.freeplane.core.extension.IExtension; import org.freeplane.core.resources.IFreeplanePropertyListener; import org.freeplane.core.resources.ResourceController; import org.freeplane.core.resources.components.IValidator; import org.freeplane.core.ui.components.UITools; import org.freeplane.core.util.FileUtils; import org.freeplane.core.util.LogUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.mode.Controller; import org.freeplane.n3.nanoxml.IXMLParser; import org.freeplane.n3.nanoxml.IXMLReader; import org.freeplane.n3.nanoxml.StdXMLReader; import org.freeplane.n3.nanoxml.XMLElement; import org.freeplane.n3.nanoxml.XMLParserFactory; import org.freeplane.n3.nanoxml.XMLWriter; /** * @author Volker Boerchers */ public class FormatController implements IExtension, IFreeplanePropertyListener { private static final String RESOURCES_NUMBER_FORMAT = "number_format"; private static final String RESOURCES_DATETIME_FORMAT = "datetime_format"; private static final String RESOURCES_DATE_FORMAT = "date_format"; private static final String FORMATS_XML = "formats.xml"; private static final String ROOT_ELEMENT = "formats"; private String pathToFile; private Locale locale; private List<PatternFormat> specialFormats = new ArrayList<PatternFormat>(); private List<PatternFormat> dateFormats = new ArrayList<PatternFormat>(); private List<PatternFormat> numberFormats = new ArrayList<PatternFormat>(); private List<PatternFormat> stringFormats = new ArrayList<PatternFormat>(); private boolean formatsLoaded; private SimpleDateFormat defaultDateFormat; private SimpleDateFormat defaultDateTimeFormat; private HashMap<String, SimpleDateFormat> dateFormatCache = new HashMap<String, SimpleDateFormat>(); private DecimalFormat defaultNumberFormat; private HashMap<String, DecimalFormat> numberFormatCache = new HashMap<String, DecimalFormat>(); static private boolean firstError = true; public IValidator createValidator (){ return new IValidator() { public ValidationResult validate(Properties properties) { final ValidationResult result = new ValidationResult(); try { createDateFormat(properties.getProperty(RESOURCES_DATE_FORMAT)); } catch (Exception e) { result.addError(TextUtils.getText("OptionPanel.validate_invalid_date_format")); } try { createDefaultDateTimeFormat(properties.getProperty(RESOURCES_DATETIME_FORMAT)); } catch (Exception e) { result.addError(TextUtils.getText("OptionPanel.validate_invalid_datetime_format")); } try { getDecimalFormat(properties.getProperty(RESOURCES_NUMBER_FORMAT)); } catch (Exception e) { result.addError(TextUtils.getText("OptionPanel.validate_invalid_number_format")); } return result; } }; } public FormatController() { final String freeplaneUserDirectory = ResourceController.getResourceController().getFreeplaneUserDirectory(); // applets have no user directory and no file access anyhow pathToFile = freeplaneUserDirectory == null ? null : freeplaneUserDirectory + File.separator + FORMATS_XML; locale = FormatUtils.getFormatLocaleFromResources(); initPatternFormats(); final ResourceController resourceController = ResourceController.getResourceController(); resourceController.addPropertyChangeListener(this); } public static FormatController getController() { return getController(Controller.getCurrentController()); } public static FormatController getController(Controller controller) { return (FormatController) controller.getExtension(FormatController.class); } public static void install(final FormatController formatController) { Controller.getCurrentController().addExtension(FormatController.class, formatController); Controller.getCurrentController().addOptionValidator(formatController.createValidator()); } private void initPatternFormats() { if (formatsLoaded) return; specialFormats.add(PatternFormat.getStandardPatternFormat()); specialFormats.add(PatternFormat.getIdentityPatternFormat()); try { if (pathToFile != null) loadFormats(); } catch (final Exception e) { LogUtils.warn(e); if(firstError){ firstError = false; UITools.errorMessage(TextUtils.getText("formats_not_loaded")); } } if (numberFormats.isEmpty() && dateFormats.isEmpty() && stringFormats.isEmpty()) { addStandardFormats(); if (pathToFile != null) saveFormatsNoThrow(); } formatsLoaded = true; } private void addStandardFormats() { String number = IFormattedObject.TYPE_NUMBER; numberFormats.add(createFormat("#0.####", PatternFormat.STYLE_DECIMAL, number, "default number", locale)); numberFormats.add(createFormat("#.00", PatternFormat.STYLE_DECIMAL, number, "decimal", locale)); numberFormats.add(createFormat("#", PatternFormat.STYLE_DECIMAL, number, "integer", locale)); numberFormats.add(createFormat("#.##%", PatternFormat.STYLE_DECIMAL, number, "percent", locale)); String dType = IFormattedObject.TYPE_DATE; final String dStyle = PatternFormat.STYLE_DATE; dateFormats.add(createLocalPattern("short date", SimpleDateFormat.SHORT, null)); dateFormats.add(createLocalPattern("medium date", SimpleDateFormat.MEDIUM, null)); dateFormats.add(createLocalPattern("short datetime", SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)); dateFormats.add(createLocalPattern("medium datetime", SimpleDateFormat.MEDIUM, SimpleDateFormat.SHORT)); dateFormats.add(createFormat("yyyy-MM-dd", dStyle, dType, "short iso date", locale)); dateFormats.add(createFormat("yyyy-MM-dd HH:mm", dStyle, dType, "long iso date", locale)); dateFormats.add(createFormat(FormattedDate.ISO_DATE_TIME_FORMAT_PATTERN, dStyle, dType, "full iso date", locale)); dateFormats.add(createFormat("HH:mm", dStyle, dType, "time", locale)); } private PatternFormat createLocalPattern(String name, int dateStyle, Integer timeStyle) { final SimpleDateFormat simpleDateFormat = (SimpleDateFormat) (timeStyle == null ? SimpleDateFormat .getDateInstance(dateStyle, locale) : SimpleDateFormat.getDateTimeInstance(dateStyle, timeStyle, locale)); final String dStyle = PatternFormat.STYLE_DATE; final String dType = IFormattedObject.TYPE_DATE; return createFormat(simpleDateFormat.toPattern(), dStyle, dType, name, locale); } private void loadFormats() throws Exception { BufferedInputStream inputStream = null; final File configXml = new File(pathToFile); if (!configXml.exists()) { LogUtils.info(pathToFile + " does not exist yet"); return; } try { final IXMLParser parser = XMLParserFactory.createDefaultXMLParser(); inputStream = new BufferedInputStream(new FileInputStream(configXml)); final IXMLReader reader = new StdXMLReader(inputStream); parser.setReader(reader); final XMLElement loader = (XMLElement) parser.parse(); final Vector<XMLElement> formats = loader.getChildren(); for (XMLElement elem : formats) { final String type = elem.getAttribute("type", null); final String style = elem.getAttribute("style", null); final String name = elem.getAttribute("name", null); final String locale = elem.getAttribute("locale", null); final String content = elem.getContent(); if (StringUtils.isEmpty(type) || StringUtils.isEmpty(style) || StringUtils.isEmpty(content)) { throw new RuntimeException("wrong format in " + configXml + ": none of the following must be empty: type=" + type + ", style=" + style + ", element content=" + content); } else { final PatternFormat format = createFormat(content, style, type, name, (locale == null ? null : new Locale(locale))); if (type.equals(IFormattedObject.TYPE_DATE)) { dateFormats.add(format); } else if (type.equals(IFormattedObject.TYPE_NUMBER)) { numberFormats.add(format); } else if (type.equals(IFormattedObject.TYPE_STRING)) { stringFormats.add(format); } else if (type.equals(PatternFormat.TYPE_STANDARD)) { // ignore this pattern that crept in in some 1.2 beta version } else { throw new RuntimeException("unknown type in " + configXml + ": type=" + type + ", style=" + style + ", element content=" + content); } } } } catch (final IOException e) { LogUtils.warn("error parsing " + configXml, e); } finally { FileUtils.silentlyClose(inputStream); } } private void saveFormatsNoThrow() { try { saveFormats(getAllFormats()); } catch (final Exception e) { LogUtils.warn("cannot create " + pathToFile, e); } } public void addPatternFormat(PatternFormat format){ specialFormats.add(format); } public ArrayList<PatternFormat> getAllFormats() { final ArrayList<PatternFormat> formats = new ArrayList<PatternFormat>(); formats.addAll(specialFormats); formats.addAll(numberFormats); formats.addAll(dateFormats); formats.addAll(stringFormats); return formats; } private void saveFormats(final List<PatternFormat> formats) throws IOException { final XMLElement saver = new XMLElement(); saver.setName(ROOT_ELEMENT); final String sep = System.getProperty("line.separator"); final String header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + sep // + "<!-- 'type' selects the kind of data the formatter is intended to format. -->" + sep // + "<!-- 'style' selects the formatter implementation: -->" + sep // + "<!-- - 'date': http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html -->" + sep // + "<!-- - 'decimal': http://download.oracle.com/javase/6/docs/api/java/text/DecimalFormat.html -->" + sep // + "<!-- - 'formatter': http://download.oracle.com/javase/6/docs/api/java/util/Formatter.html -->" + sep // + "<!-- - 'name': a informal name, a comment that's not visible in the app -->" + sep // + "<!-- - 'locale': the name of the locale, only set for locale dependent format codes -->" + sep; for (PatternFormat patternFormat : formats) { if (!patternFormat.getType().equals(PatternFormat.TYPE_IDENTITY) && !patternFormat.getType().equals(PatternFormat.TYPE_STANDARD)) { saver.addChild(patternFormat.toXml()); } } final Writer writer = new FileWriter(pathToFile); final XMLWriter xmlWriter = new XMLWriter(writer); xmlWriter.addRawContent(header); xmlWriter.write(saver, true); writer.close(); } public List<PatternFormat> getDateFormats() { return dateFormats; } public List<PatternFormat> getNumberFormats() { return numberFormats; } public List<PatternFormat> getStringFormats() { return stringFormats; } /** @deprecated use getAllFormats() instead. */ public List<String> getAllPatterns() { final ArrayList<PatternFormat> formats = getAllFormats(); ArrayList<String> result = new ArrayList<String>(formats.size()); for (PatternFormat patternFormat : formats) { result.add(patternFormat.getPattern()); } return result; } /** returns a matching {@link IFormattedObject} if possible and if <a>formatString</a> is not-null. * Otherwise <a>defaultObject</a> is returned. * Removes format if <a>formatString</a> is null. */ public static Object format(final Object obj, final String formatString, final Object defaultObject) { try { final PatternFormat format = PatternFormat.guessPatternFormat(formatString); // logging for invalid pattern is done in guessPatternFormat() if (obj == null) return obj; Object toFormat = extractObject(obj); if (format == null) return toFormat; if (toFormat instanceof String) { final String string = (String) toFormat; if (string.startsWith("=")) { return new FormattedFormula(string, formatString); } else { final ScannerController scannerController = ScannerController.getController(); if (scannerController != null) toFormat = scannerController.parse(string); } } if (format.acceptsDate() && toFormat instanceof Date) { return new FormattedDate((Date) toFormat, formatString); } else if (format.acceptsNumber() && toFormat instanceof Number) { return new FormattedNumber((Number) toFormat, formatString, format.formatObject(toFormat).toString()); } else { return new FormattedObject(toFormat, format); } } catch (Exception e) { // Be quiet, just like Excel does... // LogUtils.warn("cannot format '" + StringUtils.abbreviate(obj.toString(), 20) + "' of type " // + obj.getClass().getSimpleName() + " with " + formatString + ": " + e.getMessage()); return defaultObject; } } private static Object extractObject(final Object obj) { return (obj instanceof IFormattedObject) ? ((IFormattedObject) obj).getObject() : obj; } /** returns a matching IFormattedObject if possible and if formatString is not-null. * Otherwise <a>obj</a> is returned. * Removes format if formatString is null. */ public static Object format(final Object obj, final String formatString) { return format(obj, formatString, obj); } public static Object formatUsingDefault(final Object object) { if (object instanceof Date) return format(object, FormatController.getController().getDefaultDateTimeFormat().toPattern()); if (object instanceof Number) return format(object, FormatController.getController().getDefaultNumberFormat().toPattern()); return object; } public Format getDefaultFormat(String type) { if (type.equals(IFormattedObject.TYPE_DATE)) return getDefaultDateFormat(); else if (type.equals(IFormattedObject.TYPE_DATETIME)) return getDefaultDateTimeFormat(); else if (type.equals(IFormattedObject.TYPE_NUMBER)) return getDefaultNumberFormat(); else throw new IllegalArgumentException("unknown format style"); } public SimpleDateFormat getDefaultDateFormat() { if (defaultDateFormat != null) return defaultDateFormat; final ResourceController resourceController = ResourceController.getResourceController(); String datePattern = resourceController.getProperty(RESOURCES_DATE_FORMAT); defaultDateFormat = createDateFormat(datePattern); return defaultDateFormat; } private static SimpleDateFormat createDateFormat(final String datePattern) { final Integer style = getDateStyle(datePattern); if (style != null) return (SimpleDateFormat) DateFormat.getDateInstance(style, FormatUtils.getFormatLocaleFromResources()); else return new SimpleDateFormat(datePattern, FormatUtils.getFormatLocaleFromResources()); } public SimpleDateFormat getDefaultDateTimeFormat() { if (defaultDateTimeFormat != null) return defaultDateTimeFormat; final ResourceController resourceController = ResourceController.getResourceController(); String datetimePattern = resourceController.getProperty(RESOURCES_DATETIME_FORMAT); defaultDateTimeFormat = createDefaultDateTimeFormat(datetimePattern); return defaultDateTimeFormat; } private SimpleDateFormat createDefaultDateTimeFormat(String datetimePattern) { final String[] styles = datetimePattern.split("\\s*,\\s*"); if (styles.length == 2 && getDateStyle(styles[0]) != null && getDateStyle(styles[1]) != null) return (SimpleDateFormat) DateFormat.getDateTimeInstance(getDateStyle(styles[0]), getDateStyle(styles[1]), FormatUtils.getFormatLocaleFromResources()); else return getDateFormat(datetimePattern); } private static Integer getDateStyle(final String string) { if (string.equals("SHORT")) return DateFormat.SHORT; if (string.equals("MEDIUM")) return DateFormat.MEDIUM; if (string.equals("LONG")) return DateFormat.LONG; if (string.equals("FULL")) return DateFormat.FULL; return null; } public DecimalFormat getDefaultNumberFormat() { if (defaultNumberFormat != null) return defaultNumberFormat; final ResourceController resourceController = ResourceController.getResourceController(); defaultNumberFormat = getDecimalFormat(resourceController.getProperty(RESOURCES_NUMBER_FORMAT)); return defaultNumberFormat; } /** @param pattern either a string (see {@link DecimalFormat}) or null for a default formatter. */ public DecimalFormat getDecimalFormat(final String pattern) { DecimalFormat format = numberFormatCache.get(pattern); if (format == null) { format = (DecimalFormat) ((pattern == null) ? getDefaultNumberFormat() : new DecimalFormat(pattern, new DecimalFormatSymbols(FormatUtils.getFormatLocaleFromResources()))); numberFormatCache.put(pattern, format); } return format; } public SimpleDateFormat getDateFormat(String pattern) { SimpleDateFormat parser = dateFormatCache.get(pattern); if (parser == null) { parser = new SimpleDateFormat(pattern, FormatUtils.getFormatLocaleFromResources()); dateFormatCache.put(pattern, parser); } return parser; } public void propertyChanged(String propertyName, String newValue, String oldValue) { if (propertyName.equals(RESOURCES_DATE_FORMAT)) { defaultDateFormat = createDateFormat(newValue); final ScannerController scannerController = ScannerController.getController(); if (scannerController != null) scannerController.addParsersForStandardFormats(); } else if (propertyName.equals(RESOURCES_DATETIME_FORMAT)) { defaultDateTimeFormat = createDefaultDateTimeFormat(newValue); final ScannerController scannerController = ScannerController.getController(); if (scannerController != null) scannerController.addParsersForStandardFormats(); } else if (propertyName.equals(RESOURCES_NUMBER_FORMAT)) { defaultNumberFormat = getDecimalFormat(newValue); } else if (FormatUtils.equalsFormatLocaleName(propertyName)) { locale = FormatUtils.getFormatLocaleFromResources(); } } public List<PatternFormat> getSpecialFormats() { return specialFormats; } public PatternFormat createFormat(String pattern, String style, String type) { for(PatternFormat specialFormat : specialFormats) if (pattern.equals(specialFormat.getPattern())) return specialFormat; if (style.equals(PatternFormat.STYLE_DATE)) return new DatePatternFormat(pattern); else if (style.equals(PatternFormat.STYLE_FORMATTER)) return new FormatterPatternFormat(pattern, type); else if (style.equals(PatternFormat.STYLE_DECIMAL)) return new DecimalPatternFormat(pattern); else throw new IllegalArgumentException("unknown format style"); } public PatternFormat createFormat(final String pattern, final String style, final String type, final String name, final Locale locale) { final PatternFormat format = createFormat(pattern, style, type, name); format.setLocale(locale); return format; } public PatternFormat createFormat(final String pattern, final String style, final String type, final String name) { final PatternFormat format = createFormat(pattern, style, type); format.setName(name); return format; } }