/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2016 Open Source Geospatial Foundation (OSGeo)
* (C) 2014-2016 Boundless Spatial
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotools.ysld.encode;
import org.geotools.filter.text.ecql.ECQL;
import org.geotools.ysld.Tuple;
import org.geotools.ysld.Ysld;
import org.geotools.ysld.parse.Util;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import java.awt.Color;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Encodes a single style object as YSLD
*
* @param <T> Class of the style object
*/
public abstract class YsldEncodeHandler<T> implements Iterator<Object> {
Deque<Map<String, Object>> stack = new ArrayDeque<Map<String, Object>>();
public YsldEncodeHandler() {
reset();
}
Iterator<T> it;
YsldEncodeHandler(Iterator<T> it) {
this.it = it;
}
@SuppressWarnings("unchecked")
YsldEncodeHandler(T obj) {
this.it = obj != null ? Collections.singleton(obj).iterator()
: (Iterator<T>) Collections.emptyIterator();
}
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public Object next() {
reset();
encode(it.next());
return root();
}
protected abstract void encode(T next);
@Override
public void remove() {
throw new UnsupportedOperationException();
}
YsldEncodeHandler<T> reset() {
stack.clear();
;
stack.push(newMap());
return this;
}
YsldEncodeHandler<T> push(String key) {
Map<String, Object> map = newMap();
stack.peek().put(key, map);
stack.push(map);
return this;
}
YsldEncodeHandler<T> pop() {
stack.pop();
return this;
}
YsldEncodeHandler<T> put(String key, Object val) {
if (val != null) {
Map<String, Object> peek = stack.peek();
peek.put(key, val);
}
return this;
}
/**
* Should be used to encode values parsed with Util.name
*
* @param key
* @param expr
* @return
*/
YsldEncodeHandler<T> putName(String key, Expression expr) {
if (expr != null && expr != Expression.NIL) {
put(key, toObjOrNull(expr, true));
}
return this;
}
YsldEncodeHandler<T> put(String key, Expression expr) {
if (expr != null && expr != Expression.NIL) {
put(key, toObjOrNull(expr, false));
}
return this;
}
YsldEncodeHandler<T> put(String key, Expression e1, Expression e2) {
Tuple t = Tuple.of(toObjOrNull(e1, false), toObjOrNull(e2, false));
if (!t.isNull()) {
put(key, t);
}
return this;
}
YsldEncodeHandler<T> putColor(String key, Expression expr) {
boolean special = false;
if (expr instanceof Literal) {
// special case for color literals, drop the # so that we don't need to quote it
String str = ECQL.toCQL(expr);
str = Util.stripQuotes(str);
if (str != null && str.startsWith("#")) {
str = str.substring(1);
put(key, makeColorIfPossible(str));
special = true;
}
}
if (!special) {
put(key, expr);
}
return this;
}
YsldEncodeHandler<T> inline(YsldEncodeHandler<?> e) {
if (e.hasNext()) {
e.next();
inline(e.root());
}
return this;
}
YsldEncodeHandler<T> inline(Map<String, Object> values) {
stack.peek().putAll(values);
return this;
}
Object toColorOrNull(Expression expr) {
Object obj = toObjOrNull(expr, false);
if (obj instanceof String && expr instanceof Literal) {
String str = Util.stripQuotes(obj.toString());
obj = makeColorIfPossible(str);
}
return obj;
}
/**
* See {@link #toObjOrNull(Expression, boolean)}
* @param expr
* @return
*/
Object toObjOrNull(Expression expr) {
return toObjOrNull(expr, false);
}
static final Pattern COLOR_PATTERN = Pattern.compile("^#?([a-fA-F0-9]{6})$");
Object makeColorIfPossible(String str) {
Matcher m = COLOR_PATTERN.matcher(str);
if (m.matches()) {
// If it matches the regexp, then we know it should parse
int i = Integer.parseInt(m.group(1), 16);
return new Color(i);
} else {
return str;
}
}
static final Pattern EMBEDED_EXPRESSION_TO_ESCAPE = Pattern.compile("[$}\\\\]");
/**
* Escapes the characters '$', '}', and '\' by prepending '\'.
*/
String escapeForEmbededCQL(String s) {
return EMBEDED_EXPRESSION_TO_ESCAPE.matcher(s).replaceAll("\\\\$0");
}
/**
* Takes an {@link Expression} and encodes it as YSLD. Literals are encoded as Strings.
* Concatenation expressions are removed, as they are implicit in the YSLD syntax.
* Other non-literal expressions are wrapped in ${}.
*
* If the resulting string can be converted to the number, returns an appropriate {@link Number} object.
* Otherwise returns a {@link String}.
* Returns null if the passed expressison was null
*
*
* @param expr Expression to encode
* @param isname
* @return {@link String} or {@link Number} representation of expr, or null if expr is null.
*/
Object toObjOrNull(Expression expr, boolean isname) {
if (isNull(expr))
return null;
List<Expression> subExpressions = Util.splitConcatenates(expr);
StringBuilder builder = new StringBuilder();
for (Expression subExpr : subExpressions) {
if (isNull(subExpr)) {
// Do nothing
} else if (subExpr instanceof Literal) {
builder.append(escapeForEmbededCQL(((Literal) subExpr).getValue().toString()));
} else {
builder.append("${").append(escapeForEmbededCQL(ECQL.toCQL(subExpr))).append("}");
}
}
Object result = Util.makeNumberIfPossible(builder.toString());
return result;
}
Object toObjOrNull(String text) {
String str = text == null ? null : Util.stripQuotes(text);
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e1) {
try {
return Double.parseDouble(str);
} catch (NumberFormatException e2) {
if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) {
return Boolean.parseBoolean(str);
}
}
}
}
return text;
}
Expression nullIf(Expression expr, double value) {
return nullIf(expr, value, Double.class);
}
Expression nullIf(Expression expr, String value) {
return nullIf(expr, value, String.class);
}
<T> Expression nullIf(Expression expr, T value, Class<T> clazz) {
if (expr instanceof Literal) {
T t = expr.evaluate(null, clazz);
if (t != null && t.equals(value)) {
return null;
}
}
return expr;
}
Map<String, Object> get() {
return stack.peek();
}
Map<String, Object> root() {
return stack.getLast();
}
Map<String, Object> newMap() {
return new LinkedHashMap<String, Object>();
}
boolean isNull(Expression expr) {
return expr == null || expr == Expression.NIL;
}
protected void vendorOptions(Map<String, String> options) {
if (!options.isEmpty()) {
for (Map.Entry<String, String> kv : options.entrySet()) {
String option = Ysld.OPTION_PREFIX + kv.getKey();
String text = kv.getValue();
put(option, toObjOrNull(text));
}
}
}
}