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

Java tutorial

Introduction

Here is the source code for org.freeplane.features.format.ScannerController.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.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
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.ui.components.UITools;
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 ScannerController implements IExtension, IFreeplanePropertyListener {
    private static final String SCANNER_XML = "scanner.xml";
    private static final String ROOT_ELEMENT = "scanners";
    private String pathToFile;
    private Scanner selectedScanner;
    private static List<Scanner> scanners = new ArrayList<Scanner>();
    private static boolean scannersLoaded;

    public ScannerController() {
        final String freeplaneUserDirectory = ResourceController.getResourceController()
                .getFreeplaneUserDirectory();
        // applets have no user directory and no file access anyhow
        pathToFile = freeplaneUserDirectory == null ? null : freeplaneUserDirectory + File.separator + SCANNER_XML;
        initScanners();
        selectScanner(FormatUtils.getFormatLocaleFromResources());
        addParsersForStandardFormats();
        final ResourceController resourceController = ResourceController.getResourceController();
        resourceController.addPropertyChangeListener(this);
    }

    public static ScannerController getController() {
        return getController(Controller.getCurrentController());
    }

    public static ScannerController getController(Controller controller) {
        return (ScannerController) controller.getExtension(ScannerController.class);
    }

    public static void install(final ScannerController scannerController) {
        Controller.getCurrentController().addExtension(ScannerController.class, scannerController);
    }

    public void selectScanner(final Locale locale) {
        selectedScanner = findScanner(locale);
    }

    public Object parse(String string) {
        return selectedScanner.parse(string);
    }

    private Scanner findScanner(final Locale locale) {
        final String localeAsString = locale.toString();
        Scanner countryScanner = null;
        Scanner defaultScanner = null;
        for (Scanner scanner : scanners) {
            if (scanner.localeMatchesExactly(localeAsString))
                return scanner;
            else if (localeAsString.contains("_") && scanner.countryMatches(localeAsString))
                countryScanner = scanner;
            else if (scanner.isDefault())
                defaultScanner = scanner;
        }
        return countryScanner == null ? defaultScanner : countryScanner;
    }

    private Scanner findGoodMatch(final Locale locale) {
        final String localeAsString = locale.toString();
        Scanner countryScanner = null;
        for (Scanner scanner : scanners) {
            if (scanner.localeMatchesExactly(localeAsString))
                return scanner;
            else if (localeAsString.contains("_") && scanner.countryMatches(localeAsString))
                countryScanner = scanner;
        }
        return countryScanner;
    }

    private void initScanners() {
        if (scannersLoaded)
            return;
        scannersLoaded = true;
        try {
            if (pathToFile != null)
                loadScanners();
        } catch (final Exception e) {
            LogUtils.warn(e);
            UITools.errorMessage(TextUtils.getText("scanners_not_loaded"));
        }
        addAndSaveStandardScanners();
    }

    /** if standard formats wouldn't be parseable it would be difficult to edit recognized dates since the standard
     * format is used by the editor. */
    public void addParsersForStandardFormats() {
        final HashSet<String> patterns = new HashSet<String>();
        final List<Parser> parsers = selectedScanner.getParsers();
        for (Parser parser : parsers) {
            patterns.add(parser.getFormat());
        }
        final String standardDateFormat = FormatController.getController().getDefaultDateFormat().toPattern();
        if (!patterns.contains(standardDateFormat)) {
            selectedScanner.addParser(Parser.createParser(Parser.STYLE_DATE, IFormattedObject.TYPE_DATETIME,
                    standardDateFormat, Locale.getDefault(), "STANDARD FORMAT"));
            LogUtils.info("added parsing support for standard date format " + standardDateFormat);
        }
        final String standardDateTimeFormat = FormatController.getController().getDefaultDateTimeFormat()
                .toPattern();
        if (!patterns.contains(standardDateTimeFormat)) {
            selectedScanner.addParser(Parser.createParser(Parser.STYLE_DATE, IFormattedObject.TYPE_DATETIME,
                    standardDateTimeFormat, Locale.getDefault(), "STANDARD FORMAT"));
            LogUtils.info("added parsing support for standard date time format " + standardDateTimeFormat);
        }
        // let's hope that for every locale a proper decimal number parser is defined.
    }

    private void addAndSaveStandardScanners() {
        final int originalCount = scanners.size();
        if (findGoodMatch(new Locale("en")) == null)
            scanners.add(createScanner_en());
        if (findGoodMatch(new Locale("de")) == null)
            scanners.add(createScanner_de());
        if (findGoodMatch(new Locale("hr")) == null)
            scanners.add(createScanner_hr());
        if (findGoodMatch(Locale.getDefault()) == null) {
            // "de_DE_WIN" -> "de_DE"
            final String shortLocale = Locale.getDefault().toString().replaceAll("(.*_.*)_.*", "$1");
            scanners.add(createScanner(new Locale(shortLocale)));
        }
        if (scanners.size() != originalCount)
            saveScannersNoThrow();
    }

    private Scanner createScanner_en() {
        final Scanner s = new Scanner(new String[] { "en" }, true);
        s.setFirstChars("+-0123456789.");
        final String tNumber = IFormattedObject.TYPE_NUMBER;
        final String tDate = IFormattedObject.TYPE_DATETIME;
        final Locale loc = new Locale("en");
        s.addParser(
                Parser.createParser(Parser.STYLE_DECIMAL, tNumber, null, loc, "supports locale specific numbers"));
        // number literals are a subset of english localized decimal parser
        // s.addParser(Parser.createParser(Parser.STYLE_NUMBERLITERAL, tNumber, null, loc, "numbers like 12345.12"));
        s.addParser(
                Parser.createParser(Parser.STYLE_ISODATE, tDate, null, loc, "ISO reader for date and date/time"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "M/d", loc, "completes date with current year"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "M/d/y", loc, "parses 4/21/11 or 4/21/2011"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "M/d/y H:m", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "M/d/y H:m:s", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "H:m", loc, "parses time, sets date to today"));
        return s;
    }

    private Scanner createScanner_de() {
        final Scanner s = new Scanner(new String[] { "de" }, false);
        s.setFirstChars("+-0123456789,.");
        final String tNumber = IFormattedObject.TYPE_NUMBER;
        final String tDate = IFormattedObject.TYPE_DATETIME;
        final Locale loc = new Locale("de");
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M", loc, "completes date with current year"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y", loc, "parses 21.4.11 or 21.4.2011"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y H:m", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y H:m:s", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "H:m", loc, "parses time, sets date to today"));
        s.addParser(Parser.createParser(Parser.STYLE_DECIMAL, tNumber, null, loc,
                "uses comma as decimal separator: 1.234,12"));
        s.addParser(
                Parser.createParser(Parser.STYLE_ISODATE, tDate, null, loc, "ISO reader for date and date/time"));
        s.addParser(Parser.createParser(Parser.STYLE_NUMBERLITERAL, tNumber, null, loc,
                "support dot as decimal separator (if nothing else matches)"));
        return s;
    }

    private Scanner createScanner_hr() {
        final Scanner s = new Scanner(new String[] { "hr" }, false);
        s.setFirstChars("+-0123456789,.");
        final String tNumber = IFormattedObject.TYPE_NUMBER;
        final String tDate = IFormattedObject.TYPE_DATETIME;
        final Locale loc = new Locale("hr");
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M", loc, "completes date with current year"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y", loc, "parses 21.4.11 or 21.4.2011"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y.", loc, "parses 21.4.11. or 21.4.2011."));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y. H:m.", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "d.M.y. H:m:s", loc, "parses datetime"));
        s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate, "H:m", loc, "parses time, sets date to today"));
        s.addParser(Parser.createParser(Parser.STYLE_DECIMAL, tNumber, null, loc,
                "uses comma as decimal separator: 1.234,12"));
        s.addParser(
                Parser.createParser(Parser.STYLE_ISODATE, tDate, null, loc, "ISO reader for date and date/time"));
        s.addParser(Parser.createParser(Parser.STYLE_NUMBERLITERAL, tNumber, null, loc,
                "support dot as decimal separator (if nothing else matches)"));
        return s;
    }

    private Scanner createScanner(Locale loc) {
        final Scanner s = new Scanner(new String[] { loc.toString() }, false);
        s.setFirstChars("+-0123456789,.");
        final String tNumber = IFormattedObject.TYPE_NUMBER;
        final String tDate = IFormattedObject.TYPE_DATETIME;
        final DateFormat shortDateTimeFormat = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT,
                DateFormat.SHORT, loc);
        if (shortDateTimeFormat instanceof SimpleDateFormat) {
            s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate,
                    ((SimpleDateFormat) shortDateTimeFormat).toPattern(), loc, "short datetime format"));
        }
        final DateFormat shortDateFormat = SimpleDateFormat.getDateInstance(DateFormat.SHORT, loc);
        if (shortDateFormat instanceof SimpleDateFormat) {
            s.addParser(Parser.createParser(Parser.STYLE_DATE, tDate,
                    ((SimpleDateFormat) shortDateFormat).toPattern(), loc, "short date format"));
        }
        s.addParser(Parser.createParser(Parser.STYLE_DECIMAL, tNumber, null, loc, "number format"));
        s.addParser(
                Parser.createParser(Parser.STYLE_ISODATE, tDate, null, loc, "ISO reader for date and date/time"));
        s.addParser(Parser.createParser(Parser.STYLE_NUMBERLITERAL, tNumber, null, loc,
                "support dot as decimal separator (if nothing else matches)"));
        return s;
    }

    void loadScanners() throws Exception {
        final File configXml = new File(pathToFile);
        if (!configXml.exists()) {
            LogUtils.info(pathToFile + " does not exist yet");
            return;
        }
        try {
            final IXMLParser parser = XMLParserFactory.createDefaultXMLParser();
            final IXMLReader reader = new StdXMLReader(new BufferedInputStream(new FileInputStream(configXml)));
            parser.setReader(reader);
            final XMLElement loader = (XMLElement) parser.parse();
            final Vector<XMLElement> scannerElements = loader.getChildren();
            for (XMLElement elem : scannerElements) {
                scanners.add(parseScanner(elem));
            }
            boolean haveDefault = false;
            for (Scanner scanner : scanners) {
                if (scanner.isDefault()) {
                    if (haveDefault)
                        LogUtils.warn(configXml + ": multiple scanners are marked as default - fix that!");
                    else
                        haveDefault = true;
                }
            }
            if (!haveDefault)
                LogUtils.warn(configXml + ": no scanner is marked as default - fix that!");
        } catch (final IOException e) {
            LogUtils.warn("error parsing " + configXml, e);
        }
    }

    private Scanner parseScanner(XMLElement elem) {
        final String locales = elem.getAttribute("locale", "");
        final String isDefault = elem.getAttribute("default", "false");
        if (StringUtils.isEmpty(locales)) {
            throw new RuntimeException("wrong scanner in " + pathToFile
                    + ": none of the following must be empty: locales=" + locales + ".");
        }
        final Scanner scanner = new Scanner(locales.trim().split(","), Boolean.parseBoolean(isDefault));
        final Locale locale = new Locale(scanner.getLocales().get(0));
        for (XMLElement child : elem.getChildren()) {
            if (child.getName().equals("checkfirstchar")) {
                final String chars = elem.getAttribute("chars", "");
                final boolean disabled = Boolean.parseBoolean(elem.getAttribute("disabled", "false"));
                if (!disabled)
                    scanner.setFirstChars(chars);
            } else if (child.getName().equals("parser")) {
                scanner.addParser(parseParser(child, locale));
            }
        }
        return scanner;
    }

    private Parser parseParser(XMLElement elem, Locale locale) {
        final String type = elem.getAttribute("type", null);
        final String style = elem.getAttribute("style", null);
        final String format = elem.getAttribute("format", null);
        final String comment = elem.getAttribute("comment", null);
        return Parser.createParser(style, type, format, locale, comment);
    }

    private void saveScannersNoThrow() {
        try {
            saveScanners(scanners);
        } catch (final NoClassDefFoundError e) {
        } catch (final Exception e) {
            LogUtils.warn("cannot save create " + pathToFile, e);
        }
    }

    private void saveScanners(final List<Scanner> scanners) throws IOException {
        final XMLElement saver = new XMLElement();
        saver.setName(ROOT_ELEMENT);
        final String sep = System.getProperty("line.separator");
        final String description = commentLines("Description:" //
                , "" //
                , "<scanner> Scanners are locale dependent. If there is no scanner for" //
                , "the selected locale the scanner marked with default=\"true\" is choosen." //
                , " 'locales': A comma-separated list of locale names." //
                , "   The locale is selected via Preferences -> Environment -> Language" //
                , "   It's a pattern like 'en' (generic English) or 'en_US'" //
                , "   (English/USA). Use the more general two-letter form if appropriate." //
                , " 'default': Set to \"true\" for only one locale. The standard is 'en'." //
                , "" //
                , "<checkfirstchar> allows to enable a fast check for the first input" //
                , "character. If the first input character is not contained in the string" //
                , "given in attribute 'chars' no further attempts are made to parse the" //
                , "input as a number or date." //
                , "Do not use this option if you have have scanner formats that can" //
                , "recognize arbitrary text at the beginning of the pattern. To disable" //
                , "this check omit <checkfirstchar> or add the attribute disabled=\"true\"." //
                , " 'chars': A string of characters that may start data." //
                , "" //
                , "<type> selects the kind of data the scanner should recognize." //
                , " 'style' selects the formatter implementation:" //
                , "  - \"isodate\": flexible ISO date reader for strings like 2011-04-29 22:31:21" //
                , "    Only creates datetimes if time part is given, so no differentiation" //
                , "    between date and date/time is necessary." //
                , "  - \"date\": a special format for dates; needs attribute 'format'. See" //
                , "    http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html" //
                , "  - \"numberliteral\": parses Java float or integral number literals only, with" //
                , "    a dot as decimal separator and no thousands separator. See" //
                ,
                "    http://en.wikibooks.org/wiki/Java_Programming/Literals/Numeric_Literals/Floating_Point_Literals" //
                , "  - \"decimal\": a special format for numbers; needs attribute 'format'. See" //
                , "    http://download.oracle.com/javase/6/docs/api/java/text/DecimalFormat.html" //
                , " 'format': The format code of a \"date\" or \"decimal\" scanner." //
                , " 'comment': Inline comment, not used by the application.");
        final String header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + sep + description;
        for (Scanner scanner : scanners) {
            saver.addChild(scanner.toXml());
        }
        final Writer writer = new FileWriter(pathToFile);
        final XMLWriter xmlWriter = new XMLWriter(writer);
        xmlWriter.addRawContent(header);
        xmlWriter.write(saver, true);
        writer.close();
    }

    private String commentLines(String... comments) {
        StringBuilder builder = new StringBuilder(comments.length * 100);
        for (String comment : comments) {
            builder.append(String.format("<!-- %-71s -->%n", comment));
        }
        return builder.toString();
    }

    public void propertyChanged(String propertyName, String newValue, String oldValue) {
        if (FormatUtils.equalsFormatLocaleName(propertyName)) {
            selectScanner(FormatUtils.getFormatLocaleFromResources());
        }
    }
}