/*
* 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());
}
}
}