/*
* Copyright (C) 2008 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 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.
*
*
* Author: Steve Ratcliffe
* Create date: 02-Dec-2008
*/
package uk.me.parabola.mkgmap.osmstyle.actions;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.scan.SyntaxException;
/**
* Build a value that can have tag values substituted in it.
*
* @author Steve Ratcliffe
* @author Toby Speight
*/
public class ValueBuilder {
private static final Pattern[] FILTER_ARG_PATTERNS = {
Pattern.compile("[ \t]*([^: \\t|]+:\"[^\"]+\")[ \t]*"),
Pattern.compile("[ \t]*([^: \\t|]+:'[^']+')[ \t]*"),
// This must be last
Pattern.compile("[ \t]*([^: \\t|]+:[^|]*)"),
Pattern.compile("[ \t]*([^: \\t|]+)"),
};
private static final Pattern NAME_ARG_SPLIT = Pattern.compile("([^:]+)(?::[\"']?(.*?)[\"']?)?", Pattern.DOTALL);
private final List<ValueItem> items = new ArrayList<>();
private final boolean completeCheck;
public ValueBuilder(String pattern) {
this (pattern, true);
}
public ValueBuilder(String pattern, boolean completeCheck) {
this.completeCheck =completeCheck;
compile(pattern);
}
/**
* Build this string if all the tags that are required are available.
*
* If a tag does not exist then the whole string is rejected. This allows
* you to make conditional replacements.
*
* @param el Used as a source of tags.
* @param lel Used as a source of local tags.
* @return The built string if all required tags are available. If any
* are missing then it returns null.
*/
public String build(Element el, Element lel) {
if (completeCheck) {
// Check early for no match and return early
for (ValueItem item : items) {
if (item.getValue(el, lel) == null)
return null;
}
}
// If we get here we can build the final string. A common case
// is that there is just one, so return it directly.
if (items.size() == 1)
return items.get(0).getValue(el, lel);
// OK we have to construct the result.
StringBuilder sb = new StringBuilder();
for (ValueItem item : items)
sb.append(item.getValue(el, lel));
return sb.toString();
}
/**
* A tag value can contain variables that are the values of other tags.
* This is especially useful for 'name', as you might want to set it to
* some combination of other tags.
*
* If there are no replacement values, the same string as was passed
* in. If all the replacement values exist, then the string with the
* values all replaced. If any replacement tagname does not exist
* then returns null.
* @param in An input string that may contain tag replacement introduced
* by ${tagname}.
*/
private void compile(String in) {
if (!in.contains("$")) {
items.add(new ValueItem(in));
return;
}
char state = '\0';
StringBuilder text = new StringBuilder();
StringBuilder tagname = null;
for (char c : in.toCharArray()) {
switch (state) {
case '\0':
if (c == '$') {
state = '$';
} else
text.append(c);
break;
case '$':
switch (c) {
case '{':
case '(':
if (text.length() > 0) {
items.add(new ValueItem(text.toString()));
text.setLength(0);
}
tagname = new StringBuilder();
state = (c == '{') ? '}' : ')';
break;
default:
state = '\0';
text.append('$');
text.append(c);
}
break;
case '}':
case ')':
if (c == state) {
//noinspection ConstantConditions
assert tagname != null;
addTagValue(tagname.toString(), c == ')');
state = '\0';
tagname = null;
} else {
tagname.append(c);
}
break;
default:
assert false;
}
}
if (text.length() > 0)
items.add(new ValueItem(text.toString()));
}
private void addTagValue(String tagname, boolean is_local) {
ValueItem item = new ValueItem();
if (tagname.contains("|")) {
String[] parts = tagname.split("[ \t]*\\|", 2);
assert parts.length > 1;
item.setTagname(parts[0], is_local);
String s = parts[1];
int start = 0;
int end = s.length();
while (start < end) {
Matcher matcher = null;
for (Pattern p : FILTER_ARG_PATTERNS) {
matcher = p.matcher(s);
matcher.region(start, end);
if (matcher.lookingAt())
break;
}
if (matcher != null && matcher.lookingAt()) {
start = matcher.end() + 1;
addFilter(item, matcher.group(1));
} else {
assert false;
start = end;
}
}
} else {
item.setTagname(tagname, is_local);
}
items.add(item);
}
private void addFilter(ValueItem item, String expr) {
Matcher matcher = NAME_ARG_SPLIT.matcher(expr);
matcher.matches();
String cmd = matcher.group(1);
String arg = matcher.group(2);
switch (cmd) {
case "def":
item.addFilter(new DefaultFilter(arg));
break;
case "conv":
item.addFilter(new ConvertFilter(arg));
break;
case "subst":
item.addFilter(new SubstitutionFilter(arg));
break;
case "prefix":
item.addFilter(new PrependFilter(arg));
break;
case "highway-symbol":
item.addFilter(new HighwaySymbolFilter(arg));
break;
case "height":
item.addFilter(new HeightFilter(arg));
break;
case "not-equal":
item.addFilter(new NotEqualFilter(arg));
break;
case "substring":
item.addFilter(new SubstringFilter(arg));
break;
case "part":
item.addFilter(new PartFilter(arg));
break;
case "ascii":
item.addFilter(new TransliterateFilter("ascii"));
break;
case "latin1":
item.addFilter(new TransliterateFilter("latin1"));
break;
case "country-ISO":
item.addFilter(new CountryISOFilter());
break;
case "not-contained":
item.addFilter(new NotContainedFilter(arg));
break;
default:
throw new SyntaxException(String.format("Unknown filter '%s'", cmd));
}
}
public String toString() {
StringBuilder sb = new StringBuilder("'");
for (ValueItem v : items) {
sb.append(v);
}
sb.append("'");
return sb.toString();
}
public Set<String> getUsedTags() {
Set<String> set = new HashSet<>();
for (ValueItem v : items) {
String tagname = v.getTagname();
if (tagname != null)
set.add(tagname);
}
return set;
}
}