org.freeplane.features.format.FormatController.java Source code

Java tutorial

Introduction

Here is the source code for org.freeplane.features.format.FormatController.java

Source

/*
 *  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));
                    if (properties.getProperty(RESOURCES_DATE_FORMAT).isEmpty())
                        throw new Exception();
                } catch (Exception e) {
                    result.addError(TextUtils.getText("OptionPanel.validate_invalid_date_format"));
                }
                try {
                    createDefaultDateTimeFormat(properties.getProperty(RESOURCES_DATETIME_FORMAT));
                    if (properties.getProperty(RESOURCES_DATETIME_FORMAT).isEmpty())
                        throw new Exception();
                } catch (Exception e) {
                    result.addError(TextUtils.getText("OptionPanel.validate_invalid_datetime_format"));
                }
                try {
                    getDecimalFormat(properties.getProperty(RESOURCES_NUMBER_FORMAT));
                    if (properties.getProperty(RESOURCES_NUMBER_FORMAT).isEmpty())
                        throw new Exception();
                } 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();

        // DateFormatParser cannot handle empty date format!
        fixEmptyDataFormatProperty(resourceController, RESOURCES_DATE_FORMAT, "SHORT");

        String datePattern = resourceController.getProperty(RESOURCES_DATE_FORMAT);
        defaultDateFormat = createDateFormat(datePattern);
        return defaultDateFormat;
    }

    /**
     * Fix old invalid values (empty data format properties) on startup.
     * For new configurations, this is forced by the Validator on top of this file!
     * @param resourceController
     * @param resourceProperty
     * @param defaultValue
     */
    private void fixEmptyDataFormatProperty(final ResourceController resourceController,
            final String resourceProperty, final String defaultValue) {
        if (resourceController.getProperty(resourceProperty).isEmpty()) {
            resourceController.setProperty(resourceProperty, defaultValue);
        }
    }

    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();

        // DateFormatParser cannot handle empty date format!
        fixEmptyDataFormatProperty(resourceController, RESOURCES_DATETIME_FORMAT, "SHORT,SHORT");

        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();

        // an empty number format does not make sense!
        fixEmptyDataFormatProperty(resourceController, RESOURCES_NUMBER_FORMAT, "#0.####");

        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;
    }

}