Package org.structr.core.parser

Source Code of org.structr.core.parser.Functions

/**
* Copyright (C) 2010-2014 Morgner UG (haftungsbeschränkt)
*
* This file is part of Structr <http://structr.org>.
*
* Structr 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 3 of the
* License, or (at your option) any later version.
*
* Structr 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 Structr.  If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.core.parser;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.Normalizer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.mail.EmailException;
import org.structr.common.GraphObjectComparator;
import org.structr.common.MailHelper;
import org.structr.common.SecurityContext;
import org.structr.common.error.ErrorToken;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.SemanticErrorToken;
import org.structr.common.geo.GeoCodingResult;
import org.structr.common.geo.GeoHelper;
import org.structr.core.GraphObject;
import org.structr.core.GraphObjectMap;
import org.structr.core.Services;
import org.structr.core.app.App;
import org.structr.core.app.Query;
import org.structr.core.app.StructrApp;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.MailTemplate;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.property.ISO8601DateProperty;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.core.property.StringProperty;
import org.structr.schema.ConfigurationProvider;
import org.structr.schema.action.ActionContext;
import org.structr.schema.action.Function;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
*
* @author Christian Morgner
*/
public class Functions {

  private static final Logger logger = Logger.getLogger(Functions.class.getName());
  public static final Map<String, Function<Object, Object>> functions = new LinkedHashMap<>();

  public static final String NULL_STRING                       = "___NULL___";

  public static final String ERROR_MESSAGE_MD5                 = "Usage: ${md5(string)}. Example: ${md5(this.email)}";
  public static final String ERROR_MESSAGE_ERROR               = "Usage: ${error(...)}. Example: ${error(\"base\", \"must_equal\", int(5))}";
  public static final String ERROR_MESSAGE_UPPER               = "Usage: ${upper(string)}. Example: ${upper(this.nickName)}";
  public static final String ERROR_MESSAGE_LOWER               = "Usage: ${lower(string)}. Example: ${lower(this.email)}";
  public static final String ERROR_MESSAGE_JOIN                = "Usage: ${join(collection, separator)}. Example: ${join(this.names, \",\")}";
  public static final String ERROR_MESSAGE_CONCAT              = "Usage: ${concat(values...)}. Example: ${concat(this.firstName, this.lastName)}";
  public static final String ERROR_MESSAGE_SPLIT               = "Usage: ${split(value)}. Example: ${split(this.commaSeparatedItems)}";
  public static final String ERROR_MESSAGE_ABBR                = "Usage: ${abbr(longString, maxLength)}. Example: ${abbr(this.title, 20)}";
  public static final String ERROR_MESSAGE_CAPITALIZE          = "Usage: ${capitalize(string)}. Example: ${capitalize(this.nickName)}";
  public static final String ERROR_MESSAGE_TITLEIZE            = "Usage: ${titleize(string, separator}. Example: ${titleize(this.lowerCamelCaseString, \"_\")}";
  public static final String ERROR_MESSAGE_NUM                 = "Usage: ${num(string)}. Example: ${num(this.numericalStringValue)}";
  public static final String ERROR_MESSAGE_INT                 = "Usage: ${int(string)}. Example: ${int(this.numericalStringValue)}";
  public static final String ERROR_MESSAGE_RANDOM              = "Usage: ${random(num)}. Example: ${set(this, \"password\", random(8))}";
  public static final String ERROR_MESSAGE_RINT                = "Usage: ${rint(range)}. Example: ${rint(1000)}";
  public static final String ERROR_MESSAGE_INDEX_OF            = "Usage: ${index_of(string, word)}. Example: ${index_of(this.name, \"the\")}";
  public static final String ERROR_MESSAGE_CONTAINS            = "Usage: ${contains(string, word)}. Example: ${contains(this.name, \"the\")}";
  public static final String ERROR_MESSAGE_SUBSTRING           = "Usage: ${substring(string, start, length)}. Example: ${substring(this.name, 19, 3)}";
  public static final String ERROR_MESSAGE_LENGTH              = "Usage: ${length(string)}. Example: ${length(this.name)}";
  public static final String ERROR_MESSAGE_REPLACE             = "Usage: ${replace(template, source)}. Example: ${replace(\"${this.id}\", this)}";
  public static final String ERROR_MESSAGE_CLEAN               = "Usage: ${clean(string)}. Example: ${clean(this.stringWithNonWordChars)}";
  public static final String ERROR_MESSAGE_URLENCODE           = "Usage: ${urlencode(string)}. Example: ${urlencode(this.email)}";
  public static final String ERROR_MESSAGE_ESCAPE_JS           = "Usage: ${escape_javascript(string)}. Example: ${escape_javascript(this.name)}";
  public static final String ERROR_MESSAGE_IF                  = "Usage: ${if(condition, trueValue, falseValue)}. Example: ${if(empty(this.name), this.nickName, this.name)}";
  public static final String ERROR_MESSAGE_EMPTY               = "Usage: ${empty(string)}. Example: ${if(empty(possibleEmptyString), \"empty\", \"non-empty\")}";
  public static final String ERROR_MESSAGE_EQUAL               = "Usage: ${equal(value1, value2)}. Example: ${equal(this.children.size, 0)}";
  public static final String ERROR_MESSAGE_ADD                 = "Usage: ${add(values...)}. Example: ${add(1, 2, 3, this.children.size)}";
  public static final String ERROR_MESSAGE_INT_SUM             = "Usage: ${int_sum(list)}. Example: ${int_sum(extract(this.children, \"number\"))}";
  public static final String ERROR_MESSAGE_DOUBLE_SUM          = "Usage: ${double_sum(list)}. Example: ${double_sum(extract(this.children, \"amount\"))}";
  public static final String ERROR_MESSAGE_IS_COLLECTION       = "Usage: ${is_collection(value)}. Example: ${is_collection(this)}";
  public static final String ERROR_MESSAGE_IS_ENTITY           = "Usage: ${is_entity(value)}. Example: ${is_entity(this)}";
  public static final String ERROR_MESSAGE_EXTRACT             = "Usage: ${extract(list, propertyName)}. Example: ${extract(this.children, \"amount\")}";
  public static final String ERROR_MESSAGE_FILTER              = "Usage: ${filter(list, expression)}. Example: ${filter(this.children, gt(size(data.children), 0))}";
  public static final String ERROR_MESSAGE_MERGE               = "Usage: ${merge(list1, list2, list3, ...)}. Example: ${merge(this.children, this.siblings)}";
  public static final String ERROR_MESSAGE_UNWIND              = "Usage: ${unwind(list1, ...)}. Example: ${unwind(this.children)}";
  public static final String ERROR_MESSAGE_SORT                = "Usage: ${sort(list1, key [, true])}. Example: ${sort(this.children, \"name\")}";
  public static final String ERROR_MESSAGE_LT                  = "Usage: ${lt(value1, value2)}. Example: ${if(lt(this.children, 2), \"Less than two\", \"Equal to or more than two\")}";
  public static final String ERROR_MESSAGE_GT                  = "Usage: ${gt(value1, value2)}. Example: ${if(gt(this.children, 2), \"More than two\", \"Equal to or less than two\")}";
  public static final String ERROR_MESSAGE_LTE                 = "Usage: ${lte(value1, value2)}. Example: ${if(lte(this.children, 2), \"Equal to or less than two\", \"More than two\")}";
  public static final String ERROR_MESSAGE_GTE                 = "Usage: ${gte(value1, value2)}. Example: ${if(gte(this.children, 2), \"Equal to or more than two\", \"Less than two\")}";
  public static final String ERROR_MESSAGE_SUBT                = "Usage: ${subt(value1, value)}. Example: ${subt(5, 2)}";
  public static final String ERROR_MESSAGE_MULT                = "Usage: ${mult(value1, value)}. Example: ${mult(5, 2)}";
  public static final String ERROR_MESSAGE_QUOT                = "Usage: ${quot(value1, value)}. Example: ${quot(5, 2)}";
  public static final String ERROR_MESSAGE_ROUND               = "Usage: ${round(value1 [, decimalPlaces])}. Example: ${round(2.345678, 2)}";
  public static final String ERROR_MESSAGE_MAX                 = "Usage: ${max(value1, value2)}. Example: ${max(this.children, 10)}";
  public static final String ERROR_MESSAGE_MIN                 = "Usage: ${min(value1, value2)}. Example: ${min(this.children, 5)}";
  public static final String ERROR_MESSAGE_CONFIG              = "Usage: ${config(keyFromStructrConf)}. Example: ${config(\"base.path\")}";
  public static final String ERROR_MESSAGE_DATE_FORMAT         = "Usage: ${date_format(value, pattern)}. Example: ${date_format(this.creationDate, \"yyyy-MM-dd'T'HH:mm:ssZ\")}";
  public static final String ERROR_MESSAGE_PARSE_DATE          = "Usage: ${parse_date(value, pattern)}. Example: ${parse_format(\"2014-01-01\", \"yyyy-MM-dd\")}";
  public static final String ERROR_MESSAGE_NUMBER_FORMAT       = "Usage: ${number_format(value, ISO639LangCode, pattern)}. Example: ${number_format(12345.6789, 'en', '#,##0.00')}";
  public static final String ERROR_MESSAGE_TEMPLATE            = "Usage: ${template(name, locale, source)}. Example: ${template(\"TEXT_TEMPLATE_1\", \"en_EN\", this)}";
  public static final String ERROR_MESSAGE_NOT                 = "Usage: ${not(bool1, bool2)}. Example: ${not(\"true\", \"true\")}";
  public static final String ERROR_MESSAGE_AND                 = "Usage: ${and(bool1, bool2)}. Example: ${and(\"true\", \"true\")}";
  public static final String ERROR_MESSAGE_OR                  = "Usage: ${or(bool1, bool2)}. Example: ${or(\"true\", \"true\")}";
  public static final String ERROR_MESSAGE_GET                 = "Usage: ${get(entity, propertyKey)}. Example: ${get(this, \"children\")}";
  public static final String ERROR_MESSAGE_GET_ENTITY          = "Cannot evaluate first argument to entity, must be entity or single element list of entities.";
  public static final String ERROR_MESSAGE_SIZE                = "Usage: ${size(collection)}. Example: ${size(this.children)}";
  public static final String ERROR_MESSAGE_FIRST               = "Usage: ${first(collection)}. Example: ${first(this.children)}";
  public static final String ERROR_MESSAGE_LAST                = "Usage: ${last(collection)}. Example: ${last(this.children)}";
  public static final String ERROR_MESSAGE_NTH                 = "Usage: ${nth(collection)}. Example: ${nth(this.children, 2)}";
  public static final String ERROR_MESSAGE_GET_COUNTER         = "Usage: ${get_counter(level)}. Example: ${get_counter(1)}";
  public static final String ERROR_MESSAGE_INC_COUNTER         = "Usage: ${inc_counter(level, [resetLowerLevels])}. Example: ${inc_counter(1, true)}";
  public static final String ERROR_MESSAGE_RESET_COUNTER       = "Usage: ${reset_counter(level)}. Example: ${reset_counter(1)}";
  public static final String ERROR_MESSAGE_MERGE_PROPERTIES    = "Usage: ${merge_properties(source, target , mergeKeys...)}. Example: ${merge_properties(this, parent, \"eMail\")}";
  public static final String ERROR_MESSAGE_KEYS                = "Usage: ${keys(entity, viewName)}. Example: ${keys(this, \"ui\")}";
  public static final String ERROR_MESSAGE_EACH                = "Usage: ${each(collection, expression)}. Example: ${each(this.children, \"set(this, \"email\", lower(get(this.email))))\")}";
  public static final String ERROR_MESSAGE_STORE               = "Usage: ${store(key, value)}. Example: ${store('tmpUser', this.owner)}";
  public static final String ERROR_MESSAGE_RETRIEVE            = "Usage: ${retrieve(key)}. Example: ${retrieve('tmpUser')}";
  public static final String ERROR_MESSAGE_PRINT               = "Usage: ${print(objects...)}. Example: ${print(this.name, \"test\")}";
  public static final String ERROR_MESSAGE_READ                = "Usage: ${read(filename)}. Example: ${read(\"text.xml\")}";
  public static final String ERROR_MESSAGE_WRITE               = "Usage: ${write(filename, value)}. Example: ${write(\"text.txt\", this.name)}";
  public static final String ERROR_MESSAGE_APPEND              = "Usage: ${append(filename, value)}. Example: ${append(\"test.txt\", this.name)}";
  public static final String ERROR_MESSAGE_XML                 = "Usage: ${xml(xmlSource)}. Example: ${xpath(xml(this.xmlSource), \"/test/testValue\")}";
  public static final String ERROR_MESSAGE_XPATH               = "Usage: ${xpath(xmlDocument, expression)}. Example: ${xpath(xml(this.xmlSource), \"/test/testValue\")}";
  public static final String ERROR_MESSAGE_SET                 = "Usage: ${set(entity, propertyKey, value)}. Example: ${set(this, \"email\", lower(this.email))}";
  public static final String ERROR_MESSAGE_SEND_PLAINTEXT_MAIL = "Usage: ${send_plaintext_mail(fromAddress, fromName, toAddress, toName, subject, content)}.";
  public static final String ERROR_MESSAGE_SEND_HTML_MAIL      = "Usage: ${send_html_mail(fromAddress, fromName, toAddress, toName, subject, content)}.";
  public static final String ERROR_MESSAGE_GEOCODE             = "Usage: ${geocode(street, city, country)}. Example: ${set(this, geocode(this.street, this.city, this.country))}";
  public static final String ERROR_MESSAGE_FIND                = "Usage: ${find(type, key, value)}. Example: ${find(\"User\", \"email\", \"tester@test.com\"}";
  public static final String ERROR_MESSAGE_CREATE              = "Usage: ${create(type, key, value)}. Example: ${create(\"Feedback\", \"text\", this.text)}";
  public static final String ERROR_MESSAGE_DELETE              = "Usage: ${delete(entity)}. Example: ${delete(this)}";
  public static final String ERROR_MESSAGE_CACHE               = "Usage: ${cache(key, timeout, valueExpression)}. Example: ${cache('value', 60, GET('http://rate-limited-URL.com'))}";

  public static Function<Object, Object> get(final String name) {
    return functions.get(name);
  }

  public static Object evaluate(final SecurityContext securityContext, final ActionContext actionContext, final GraphObject entity, final String expression) throws FrameworkException {

    final String expressionWithoutNewlines = expression.replace('\n', ' ');
    final StreamTokenizer tokenizer        = new StreamTokenizer(new StringReader(expressionWithoutNewlines));
    tokenizer.eolIsSignificant(true);
    tokenizer.wordChars('_', '_');
    tokenizer.wordChars('.', '.');
    tokenizer.wordChars('!', '!');

    Expression root    = new RootExpression();
    Expression current = root;
    Expression next    = null;
    String lastToken   = null;
    int token          = 0;
    int level          = 0;

    while (token != StreamTokenizer.TT_EOF) {

      token = nextToken(tokenizer);

      switch (token) {

        case StreamTokenizer.TT_EOF:
          break;

        case StreamTokenizer.TT_EOL:
          break;

        case StreamTokenizer.TT_NUMBER:
          if (current == null) {
            throw new FrameworkException(422, "Invalid expression: mismatched opening bracket before NUMBER");
          }
          next = new ConstantExpression(tokenizer.nval);
          current.add(next);
          lastToken += "NUMBER";
          break;

        case StreamTokenizer.TT_WORD:
          if (current == null) {
            throw new FrameworkException(422, "Invalid expression: mismatched opening bracket before " + tokenizer.sval);
          }
          next = checkReservedWords(tokenizer.sval);
          current.add(next);
          lastToken = tokenizer.sval;
          break;

        case '(':
          if (((current == null || current instanceof RootExpression) && next == null) || current == next) {

            // an additional bracket without a new function,
            // this can only be an execution group.
            next = new GroupExpression();
            current.add(next);
          }
          current = next;
          lastToken += "(";
          level++;
          break;

        case ')':
          if (current == null) {
            throw new FrameworkException(422, "Invalid expression: mismatched opening bracket before " + lastToken);
          }
          current = current.getParent();
          if (current == null) {
            throw new FrameworkException(422, "Invalid expression: mismatched closing bracket after " + lastToken);
          }
          lastToken += ")";
          level--;
          break;

        case ';':
          next = null;
          lastToken += ";";
          break;

        case ',':
          next = current;
          lastToken += ",";
          break;

        default:
          if (current == null) {
            throw new FrameworkException(422, "Invalid expression: mismatched opening bracket before " + tokenizer.sval);
          }
          current.add(new ConstantExpression(tokenizer.sval));
          lastToken = tokenizer.sval;

      }
    }

    if (level > 0) {
      throw new FrameworkException(422, "Invalid expression: mismatched closing bracket after " + lastToken);
    }

    return root.evaluate(securityContext, actionContext, entity);
  }

  private static Expression checkReservedWords(final String word) throws FrameworkException {

    if (word == null) {
      return new NullExpression();
    }

    switch (word) {

      case "cache":
        return new CacheExpression();

      case "true":
        return new ConstantExpression(true);

      case "false":
        return new ConstantExpression(false);

      case "if":
        return new IfExpression();

      case "each":
        return new EachExpression();

      case "filter":
        return new FilterExpression();

      case "data":
        return new ValueExpression("data");

      case "null":
        return new ConstantExpression(NULL_STRING);
    }

    // no match, try functions
    final Function<Object, Object> function = Functions.get(word);
    if (function != null) {

      return new FunctionExpression(word, function);

    } else {

      return new ValueExpression(word);
    }
  }

  private static int nextToken(final StreamTokenizer tokenizer) {

    try {

      return tokenizer.nextToken();

    } catch (IOException ioex) { }

    return StreamTokenizer.TT_EOF;
  }

  static {

    functions.put("error", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          final PropertyKey key = StructrApp.getConfiguration().getPropertyKeyForJSONName(entity.getClass(), sources[0].toString());
          ctx.raiseError(entity.getType(), new ErrorToken(422, key) {

            @Override
            public JsonElement getContent() {
              return new JsonPrimitive(getErrorToken());
            }

            @Override
            public String getErrorToken() {
              return sources[1].toString();
            }
          });


        } else if (arrayHasLengthAndAllElementsNotNull(sources, 3)) {

          final PropertyKey key = StructrApp.getConfiguration().getPropertyKeyForJSONName(entity.getClass(), sources[0].toString());
          ctx.raiseError(entity.getType(), new SemanticErrorToken(key) {

            @Override
            public JsonElement getContent() {

              JsonObject obj = new JsonObject();

              if (sources[2] instanceof Number) {

                obj.add(getErrorToken(), new JsonPrimitive((Number)sources[2]));

              } else if (sources[2] instanceof Boolean) {

                obj.add(getErrorToken(), new JsonPrimitive((Boolean)sources[2]));

              } else {

                obj.add(getErrorToken(), new JsonPrimitive(sources[2].toString()));
              }

              return obj;
            }

            @Override
            public String getErrorToken() {
              return sources[1].toString();
            }
          });
        }

        return null;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_ERROR;
      }
    });
    functions.put("md5", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? DigestUtils.md5Hex(sources[0].toString())
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MD5;
      }
    });
    functions.put("upper", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? sources[0].toString().toUpperCase()
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_UPPER;
      }

    });
    functions.put("lower", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? sources[0].toString().toLowerCase()
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_LOWER;
      }

    });
    functions.put("join", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof Collection) {

          return StringUtils.join((Collection)sources[0], sources[1].toString());
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_JOIN;
      }

    });
    functions.put("concat", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        final List list = new ArrayList();
        for (final Object source : sources) {

          if (source instanceof Collection) {

            list.addAll((Collection)source);

          } else {

            list.add(source);
          }
        }

        return StringUtils.join(list, "");
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CONCAT;
      }

    });
    functions.put("split", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          final String toSplit = sources[0].toString();
          String splitExpr     = "[,;]+";

          if (sources.length >= 2) {
            splitExpr = sources[1].toString();
          }

          return Arrays.asList(toSplit.split(splitExpr));
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SPLIT;
      }

    });
    functions.put("abbr", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          try {
            int maxLength = Double.valueOf(sources[1].toString()).intValue();

            if (sources[0].toString().length() > maxLength) {

              return StringUtils.substringBeforeLast(StringUtils.substring(sources[0].toString(), 0, maxLength), " ").concat("…");

            } else {

              return sources[0];
            }

          } catch (NumberFormatException nfe) {

            return nfe.getMessage();

          }

        }

        return "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_ABBR;
      }

    });
    functions.put("capitalize", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? StringUtils.capitalize(sources[0].toString())
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CAPITALIZE;
      }
    });
    functions.put("titleize", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources == null || sources.length < 2 || sources[0] == null) {
          return null;
        }

        if (StringUtils.isBlank(sources[0].toString())) {
          return "";
        }

        if (sources[1] == null) {
          sources[1] = " ";
        }

        String[] in = StringUtils.split(sources[0].toString(), sources[1].toString());
        String[] out = new String[in.length];
        for (int i = 0; i < in.length; i++) {
          out[i] = StringUtils.capitalize(in[i]);
        }
        return StringUtils.join(out, " ");

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_TITLEIZE;
      }

    });
    functions.put("num", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          try {
            return getDoubleOrNull(sources[0]);

          } catch (Throwable t) {
            // ignore
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_NUM;
      }
    });
    functions.put("int", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          if (sources[0] instanceof Number) {
            return ((Number)sources[0]).intValue();
          }

          try {
            return getDoubleOrNull(sources[0]).intValue();

          } catch (Throwable t) {
            // ignore
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_INT;
      }
    });
    functions.put("random", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) && sources[0] instanceof Number) {

          try {
            return RandomStringUtils.randomAlphanumeric(((Number)sources[0]).intValue());

          } catch (Throwable t) {
            // ignore
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_RANDOM;
      }
    });
    functions.put("rint", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) && sources[0] instanceof Number) {

          try {
            return new Random(System.currentTimeMillis()).nextInt(((Number)sources[0]).intValue());

          } catch (Throwable t) {
            // ignore
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_RINT;
      }
    });
    functions.put("index_of", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          final String source = sources[0].toString();
          final String part   = sources[1].toString();

          return source.indexOf(part);
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_INDEX_OF;
      }
    });
    functions.put("contains", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          final String source = sources[0].toString();
          final String part   = sources[1].toString();

          return source.contains(part);
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CONTAINS;
      }
    });
    functions.put("substring", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          final String source    = sources[0].toString();
          final int sourceLength = source.length();
          final int start        = parseInt(sources[1]);
          final int length       = sources.length >= 3 ? parseInt(sources[2]) : sourceLength - start;
          final int end          = start + length;

          if (start >= 0 && start < sourceLength && end >= 0 && end <= sourceLength && start <= end) {

            return source.substring(start, end);
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SUBSTRING;
      }
    });
    functions.put("length", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          return sources[0].toString().length();
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SUBSTRING;
      }
    });
    functions.put("replace", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          final String template = sources[0].toString();
          AbstractNode node     = null;

          if (sources[1] instanceof AbstractNode) {
            node = (AbstractNode)sources[1];
          }

          if (sources[1] instanceof List) {

            final List list = (List)sources[1];
            if (list.size() == 1 && list.get(0) instanceof AbstractNode) {

              node = (AbstractNode)list.get(0);
            }
          }

          if (node != null) {

            // recursive replacement call, be careful here
            return node.replaceVariables(entity.getSecurityContext(), ctx, template);
          }

          return "";
        }

        return usage();

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_REPLACE;
      }
    });
    functions.put("clean", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          if (StringUtils.isBlank(sources[0].toString())) {
            return "";
          }

          return cleanString(sources[0]);
        }

        return null;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CLEAN;
      }

    });
    functions.put("urlencode", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? encodeURL(sources[0].toString())
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_URLENCODE;
      }

    });
    functions.put("escape_javascript", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        return (arrayHasMinLengthAndAllElementsNotNull(sources, 1))
          ? StringEscapeUtils.escapeEcmaScript(sources[0].toString())
          : "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_ESCAPE_JS;
      }

    });
    functions.put("if", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources[0] == null || sources.length < 3) {

          return "";
        }

        if ("true".equals(sources[0]) || Boolean.TRUE.equals(sources[0])) {

          return sources[1];

        } else {

          return sources[2];
        }

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_IF;
      }

    });
    functions.put("empty", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources.length == 0 || sources[0] == null || StringUtils.isEmpty(sources[0].toString())) {

          return true;

        } else {
          return false;
        }

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_EMPTY;
      }

    });
    functions.put("equal", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        logger.log(Level.FINE, "Length: {0}", sources.length);

        if (sources.length < 2) {

          return true;
        }

        logger.log(Level.FINE, "Comparing {0} to {1}", new java.lang.Object[]{sources[0], sources[1]});

        if (sources[0] == null && sources[1] == null) {
          return true;
        }

        if (sources[0] == null || sources[1] == null) {
          return false;
        }

        return valueEquals(sources[0], sources[1]);
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_EQUAL;
      }

    });
    functions.put("add", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        Double result = 0.0;

        if (sources != null) {

          for (Object i : sources) {

            if (i != null) {

              try {

                result += Double.parseDouble(i.toString());

              } catch (Throwable t) {

                return t.getMessage();

              }

            } else {

              result += 0.0;
            }
          }

        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_ADD;
      }

    });
    functions.put("double_sum", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        double result = 0.0;

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          if (sources[0] instanceof Collection) {

            for (final Number num : (Collection<Number>)sources[0]) {

              result += num.doubleValue();
            }
          }
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_DOUBLE_SUM;
      }

    });
    functions.put("int_sum", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        int result = 0;

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          if (sources[0] instanceof Collection) {

            for (final Number num : (Collection<Number>)sources[0]) {

              result += num.intValue();
            }
          }
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_INT_SUM;
      }

    });
    functions.put("is_collection", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {
          return (sources[0] instanceof Collection);
        } else {
          return false;
        }

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_IS_COLLECTION;
      }

    });
    functions.put("is_entity", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {
          return (sources[0] instanceof GraphObject);
        } else {
          return false;
        }

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_IS_ENTITY;
      }

    });
    functions.put("extract", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          // no property key given, maybe we should extract a list of lists?
          if (sources[0] instanceof Collection) {

            final List extraction = new LinkedList();

            for (final Object obj : (Collection)sources[0]) {

              if (obj instanceof Collection) {

                extraction.addAll((Collection)obj);
              }
            }

            return extraction;
          }

        } else if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          if (sources[0] instanceof Collection && sources[1] instanceof String) {

            final ConfigurationProvider config = StructrApp.getConfiguration();
            final List extraction              = new LinkedList();
            final String keyName               = (String)sources[1];

            for (final Object obj : (Collection)sources[0]) {

              if (obj instanceof GraphObject) {

                final PropertyKey key = config.getPropertyKeyForJSONName(obj.getClass(), keyName);
                final Object value = ((GraphObject)obj).getProperty(key);
                if (value != null) {

                  extraction.add(value);
                }
              }
            }

            return extraction;
          }
        }

        return null;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_EXTRACT;
      }

    });
    functions.put("merge", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        final List list = new ArrayList();
        for (final Object source : sources) {

          if (source instanceof Collection) {

            // filter null objects
            for (Object obj : (Collection)source) {
              if (obj != null) {

                list.add(obj);
              }
            }

          } else if (source != null) {

            list.add(source);
          }
        }

        return list;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MERGE;
      }

    });
    functions.put("unwind", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        final List list = new ArrayList();
        for (final Object source : sources) {

          if (source instanceof Collection) {

            // filter null objects
            for (Object obj : (Collection)source) {
              if (obj != null) {

                if (obj instanceof Collection) {

                  for (final Object elem : (Collection)obj) {

                    if (elem != null) {

                      list.add(elem);
                    }
                  }

                } else {

                  list.add(obj);
                }
              }
            }

          } else if (source != null) {

            list.add(source);
          }
        }

        return list;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MERGE;
      }

    });
    functions.put("sort", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          if (sources[0] instanceof List && sources[1] instanceof String) {

            final List list         = (List)sources[0];
            final String sortKey    = sources[1].toString();
            final Iterator iterator = list.iterator();

            if (iterator.hasNext()) {

              final Object firstElement = iterator.next();
              if (firstElement instanceof GraphObject) {

                final Class type          = firstElement.getClass();
                final PropertyKey key     = StructrApp.getConfiguration().getPropertyKeyForJSONName(type, sortKey);
                final boolean descending  = sources.length == 3 && sources[2] != null && "true".equals(sources[2].toString());

                if (key != null) {

                  List<GraphObject> sortCollection = (List<GraphObject>)list;
                  Collections.sort(sortCollection, new GraphObjectComparator(key, descending));
                }
              }

            }
          }
        }

        return sources[0];
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SORT;
      }

    });
    functions.put("lt", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        String result = "";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          double value1 = getDoubleForComparison(sources[0]);
          double value2 = getDoubleForComparison(sources[1]);

          return value1 < value2;
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_LT;
      }
    });
    functions.put("gt", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        String result = "";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          double value1 = getDoubleForComparison(sources[0]);
          double value2 = getDoubleForComparison(sources[1]);

          return value1 > value2;
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_GT;
      }
    });
    functions.put("lte", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        String result = "";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          double value1 = getDoubleForComparison(sources[0]);
          double value2 = getDoubleForComparison(sources[1]);

          return value1 <= value2;
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_LTE;
      }
    });
    functions.put("gte", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        String result = "";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          double value1 = getDoubleForComparison(sources[0]);
          double value2 = getDoubleForComparison(sources[1]);

          return value1 >= value2;
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_GTE;
      }
    });
    functions.put("subt", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          try {

            Double result = Double.parseDouble(sources[0].toString());

            for (int i = 1; i < sources.length; i++) {

              result -= Double.parseDouble(sources[i].toString());

            }

            return result;

          } catch (Throwable t) {

            return t.getMessage();

          }
        }

        return "";

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SUBT;
      }
    });
    functions.put("mult", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        Double result = 1.0d;

        if (sources != null) {

          for (Object i : sources) {

            try {

              result *= Double.parseDouble(i.toString());

            } catch (Throwable t) {

              return t.getMessage();

            }
          }

        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MULT;
      }
    });
    functions.put("quot", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          try {

            return Double.parseDouble(sources[0].toString()) / Double.parseDouble(sources[1].toString());

          } catch (Throwable t) {

            return t.getMessage();

          }

        } else {

          if (sources != null) {

            if (sources.length > 0 && sources[0] != null) {
              return Double.valueOf(sources[0].toString());
            }

            return "";
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_QUOT;
      }
    });
    functions.put("round", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          if (StringUtils.isBlank(sources[0].toString())) {
            return "";
          }

          try {

            Double f1 = Double.parseDouble(sources[0].toString());
            double f2 = Math.pow(10, (Double.parseDouble(sources[1].toString())));
            long r = Math.round(f1 * f2);

            return (double) r / f2;

          } catch (Throwable t) {

            return t.getMessage();

          }

        } else {

          return "";
        }
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_ROUND;
      }
    });
    functions.put("max", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        Object result   = "";
        String errorMsg = "ERROR! Usage: ${max(val1, val2)}. Example: ${max(5,10)}";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          try {
            result = Math.max(Double.parseDouble(sources[0].toString()), Double.parseDouble(sources[1].toString()));

          } catch (Throwable t) {

            logger.log(Level.WARNING, "Could not determine max() of {0} and {1}", new Object[]{sources[0], sources[1]});
            result = errorMsg;
          }

        } else {

          result = "";
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MAX;
      }
    });
    functions.put("min", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        Object result   = "";
        String errorMsg = "ERROR! Usage: ${min(val1, val2)}. Example: ${min(5,10)}";

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          try {
            result = Math.min(Double.parseDouble(sources[0].toString()), Double.parseDouble(sources[1].toString()));

          } catch (Throwable t) {

            logger.log(Level.WARNING, "Could not determine min() of {0} and {1}", new Object[]{sources[0], sources[1]});
            result = errorMsg;
          }

        } else {

          result = "";
        }

        return result;

      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MIN;
      }
    });
    functions.put("config", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          final String configKey    = sources[0].toString();
          final String defaultValue = sources.length >= 2 ? sources[1].toString() : "";

          return StructrApp.getConfigurationValue(configKey, defaultValue);
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CONFIG;
      }
    });
    functions.put("date_format", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources == null || sources != null && sources.length != 2) {
          return ERROR_MESSAGE_DATE_FORMAT;
        }

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          Date date = null;

          if (sources[0] instanceof Date) {

            date = (Date)sources[0];

          } else if (sources[0] instanceof Number) {

            date = new Date(((Number)sources[0]).longValue());

          } else {

            try {

              // parse with format from IS
              date = new SimpleDateFormat(ISO8601DateProperty.PATTERN).parse(sources[0].toString());

            } catch (ParseException ex) {
              ex.printStackTrace();
            }

          }

          // format with given pattern
          return new SimpleDateFormat(sources[1].toString()).format(date);
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_DATE_FORMAT;
      }
    });
    functions.put("parse_date", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources == null || sources != null && sources.length != 2) {
          return ERROR_MESSAGE_PARSE_DATE;
        }

        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          String dateString = sources[0].toString();

          if (StringUtils.isBlank(dateString)) {
            return "";
          }

          String pattern = sources[1].toString();

          try {
            // parse with format from IS
            return new SimpleDateFormat(pattern).parse(dateString);

          } catch (ParseException ex) {
            logger.log(Level.WARNING, "Could not parse date " + dateString + " and format it to pattern " + pattern, ex);
          }

        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_PARSE_DATE;
      }
    });
    functions.put("number_format", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources == null || sources != null && sources.length != 3) {
          return ERROR_MESSAGE_NUMBER_FORMAT;
        }

        if (arrayHasLengthAndAllElementsNotNull(sources, 3)) {

          if (StringUtils.isBlank(sources[0].toString())) {
            return "";
          }

          try {

            Double val = Double.parseDouble(sources[0].toString());
            String langCode = sources[1].toString();
            String pattern = sources[2].toString();

            return new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(Locale.forLanguageTag(langCode))).format(val);

          } catch (Throwable t) {   }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_NUMBER_FORMAT;
      }
    });
    functions.put("template", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources == null || sources != null && sources.length != 3) {
          return ERROR_MESSAGE_TEMPLATE;
        }

        if (arrayHasLengthAndAllElementsNotNull(sources, 3) && sources[2] instanceof AbstractNode) {

          final App app                       = StructrApp.getInstance(entity.getSecurityContext());
          final String name                   = sources[0].toString();
          final String locale                 = sources[1].toString();
          final MailTemplate template         = app.nodeQuery(MailTemplate.class).andName(name).and(MailTemplate.locale, locale).getFirst();
          final AbstractNode templateInstance = (AbstractNode)sources[2];

          if (template != null) {

            final String text = template.getProperty(MailTemplate.text);
            if (text != null) {

              // recursive replacement call, be careful here
              return templateInstance.replaceVariables(entity.getSecurityContext(), ctx, text);
            }
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_TEMPLATE;
      }
    });
    functions.put("not", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          return !("true".equals(sources[0].toString()) || Boolean.TRUE.equals(sources[0]));

        }

        return true;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_NOT;
      }

    });
    functions.put("and", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        boolean result = true;

        if (sources != null) {

          if (sources.length < 2) {
            return usage();
          }

          for (Object i : sources) {

            if (i != null) {

              try {

                result &= "true".equals(i.toString()) || Boolean.TRUE.equals(i);

              } catch (Throwable t) {

                return t.getMessage();

              }

            } else {

              // null is false
              return false;
            }
          }

        }

        return result;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_AND;
      }

    });
    functions.put("or", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        boolean result = false;

        if (sources != null) {

          if (sources.length < 2) {
            return usage();
          }

          for (Object i : sources) {

            if (i != null) {

              try {

                result |= "true".equals(i.toString()) || Boolean.TRUE.equals(i);

              } catch (Throwable t) {

                return t.getMessage();

              }

            } else {

              // null is false
              result |= false;
            }
          }

        }

        return result;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_OR;
      }
    });
    functions.put("get", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        final SecurityContext securityContext = entity.getSecurityContext();
        if (arrayHasLengthAndAllElementsNotNull(sources, 2)) {

          GraphObject dataObject = null;

          if (sources[0] instanceof GraphObject) {
            dataObject = (GraphObject)sources[0];
          }

          if (sources[0] instanceof List) {

            final List list = (List)sources[0];
            if (list.size() == 1 && list.get(0) instanceof GraphObject) {

              dataObject = (GraphObject)list.get(0);
            }
          }

          if (dataObject != null) {

            final String keyName     = sources[1].toString();
            final PropertyKey key    = StructrApp.getConfiguration().getPropertyKeyForJSONName(dataObject.getClass(), keyName);

            if (key != null) {

              final PropertyConverter inputConverter = key.inputConverter(securityContext);
              Object value = dataObject.getProperty(key);

              if (inputConverter != null) {
                return inputConverter.revert(value);
              }

              return dataObject.getProperty(key);
            }

            return "";

          } else {

            return ERROR_MESSAGE_GET_ENTITY;
          }
        }

        return usage();
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_GET;
      }
    });
    functions.put("size", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        final List list = new ArrayList();
        for (final Object source : sources) {

          if (source instanceof Collection) {

            // filter null objects
            for (Object obj : (Collection)source) {
              if (obj != null && !NULL_STRING.equals(obj)) {

                list.add(obj);
              }
            }

          } else if (source != null && !NULL_STRING.equals(source)) {

            list.add(source);
          }

          return list.size();
        }

        return null;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SIZE;
      }
    });
    functions.put("first", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) && sources[0] instanceof List && !((List)sources[0]).isEmpty()) {
          return ((List)sources[0]).get(0);
        }

        return null;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_FIRST;
      }
    });
    functions.put("last", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) &&  sources[0] instanceof List && !((List)sources[0]).isEmpty()) {

          final List list = (List)sources[0];
          return list.get(list.size() - 1);
        }

        return null;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_LAST;
      }
    });
    functions.put("nth", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof List && !((List)sources[0]).isEmpty()) {

          final List list = (List)sources[0];
          final int pos   = Double.valueOf(sources[1].toString()).intValue();
          final int size  = list.size();

          if (pos >= size) {

            return null;

          }

          return list.get(Math.min(Math.max(0, pos), size-1));
        }

        return null;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_NTH;
      }
    });
    functions.put("get_counter", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          return ctx.getCounter(parseInt(sources[0]));
        }

        return 0;
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_GET_COUNTER;
      }
    });
    functions.put("inc_counter", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          final int level = parseInt(sources[0]);

          ctx.incrementCounter(level);

          // reset lower levels?
          if (sources.length == 2 && "true".equals(sources[1].toString())) {

            // reset lower levels
            for (int i=level+1; i<10; i++) {
              ctx.resetCounter(i);
            }
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_INC_COUNTER;
      }
    });
    functions.put("reset_counter", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          ctx.resetCounter(parseInt(sources[0]));
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_RESET_COUNTER;
      }
    });
    functions.put("merge_properties", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof GraphObject && sources[1] instanceof GraphObject) {

          final ConfigurationProvider config = StructrApp.getConfiguration();
          final Set<PropertyKey> mergeKeys   = new LinkedHashSet<>();
          final GraphObject source           = (GraphObject)sources[0];
          final GraphObject target           = (GraphObject)sources[1];
          final int paramCount               = sources.length;

          for (int i=2; i<paramCount; i++) {

            final String keyName     = sources[i].toString();
            final PropertyKey key    = config.getPropertyKeyForJSONName(target.getClass(), keyName);

            mergeKeys.add(key);
          }

          for (final PropertyKey key : mergeKeys) {

            final Object sourceValue = source.getProperty(key);
            if (sourceValue != null) {

              target.setProperty(key, sourceValue);
            }

          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_MERGE_PROPERTIES;
      }
    });
    functions.put("keys", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof GraphObject) {

          final Set<String> keys   = new LinkedHashSet<>();
          final GraphObject source = (GraphObject)sources[0];

          for (final PropertyKey key : source.getPropertyKeys(sources[1].toString())) {
            keys.add(key.jsonName());
          }

          return new LinkedList<>(keys);
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_KEYS;
      }
    });

    // ----- BEGIN functions with side effects -----
    functions.put("retrieve", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) && sources[0] instanceof String) {

          return ctx.retrieve(sources[0].toString());

        } else {

          return ERROR_MESSAGE_RETRIEVE;
        }
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_RETRIEVE;
      }
    });
    functions.put("store", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof String) {

          ctx.store(sources[0].toString(), sources[1]);

        } else {

          return ERROR_MESSAGE_STORE;
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_STORE;
      }
    });
    functions.put("print", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources != null) {

          for (Object i : sources) {

            System.out.print(i);
          }

          System.out.println();
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_PRINT;
      }
    });
    functions.put("read", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1)) {

          try {
            final String sandboxFilename = getSandboxFileName(sources[0].toString());
            if (sandboxFilename != null) {

              final File file = new File(sandboxFilename);
              if (file.exists() && file.length() < 10000000) {

                try (final FileInputStream fis = new FileInputStream(file)) {

                  return IOUtils.toString(fis, "utf-8");
                }
              }
            }

          } catch (IOException ioex) {
            ioex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_READ;
      }
    });
    functions.put("write", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          try {
            final String sandboxFilename = getSandboxFileName(sources[0].toString());
            if (sandboxFilename != null) {

              final File file = new File(sandboxFilename);
              if (!file.exists()) {

                try (final Writer writer = new OutputStreamWriter(new FileOutputStream(file, false))) {

                  for (int i=1; i<sources.length; i++) {
                    if (sources[i] != null) {
                      IOUtils.write(sources[i].toString(), writer);
                    }
                  }

                  writer.flush();
                }

              } else {

                logger.log(Level.SEVERE, "Trying to overwrite an existing file, please use append() for that purpose.");
              }
            }

          } catch (IOException ioex) {
            ioex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_WRITE;
      }
    });
    functions.put("append", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 1)) {

          try {
            final String sandboxFilename = getSandboxFileName(sources[0].toString());
            if (sandboxFilename != null) {

              final File file = new File(sandboxFilename);

              try (final Writer writer = new OutputStreamWriter(new FileOutputStream(file, true))) {

                for (int i=1; i<sources.length; i++) {
                  IOUtils.write(sources[i].toString(), writer);
                }

                writer.flush();
              }
            }

          } catch (IOException ioex) {
            ioex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_APPEND;
      }
    });
    functions.put("xml", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 1) && sources[0] instanceof String) {

          try {

            final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            if (builder != null) {

              final String xml          = (String)sources[0];
              final StringReader reader = new StringReader(xml);
              final InputSource src     = new InputSource(reader);

              return builder.parse(src);
            }

          } catch (IOException | SAXException | ParserConfigurationException ex) {
            ex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_XML;
      }
    });
    functions.put("xpath", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2) && sources[0] instanceof Document) {

          try {

            XPath xpath = XPathFactory.newInstance().newXPath();
            return xpath.evaluate(sources[1].toString(), sources[0], XPathConstants.STRING);

          } catch (XPathExpressionException ioex) {
            ioex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_XPATH;
      }
    });
    functions.put("set", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 2)) {

          if (sources[0] instanceof GraphObject) {

            final GraphObject source              = (GraphObject)sources[0];
            final Map<String, Object> properties  = new LinkedHashMap<>();
            final SecurityContext securityContext = source.getSecurityContext();
            final Gson gson                       = new GsonBuilder().create();
            final Class type                      = source.getClass();
            final int sourceCount                 = sources.length;

            if (sources.length == 3 && sources[2] != null && sources[1].toString().matches("[a-zA-Z0-9_]+")) {

              properties.put(sources[1].toString(), sources[2]);

            } else {

              // we either have and odd number of items, or two multi-value items.
              for (int i=1; i<sourceCount; i++) {

                final Map<String, Object> values = deserialize(gson, sources[i].toString());
                if (values != null) {

                  properties.putAll(values);
                }
              }
            }

            // store values in entity
            final PropertyMap map = PropertyMap.inputTypeToJavaType(securityContext, type, properties);
            for (final Map.Entry<PropertyKey, Object> entry : map.entrySet()) {

              source.setProperty(entry.getKey(), entry.getValue());
            }

          } else {

            throw new FrameworkException(422, "Invalid use of builtin method set, usage: set(entity, params..)");
          }

        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SET;
      }
    });
    functions.put("send_plaintext_mail", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 6)) {

          final String from        = sources[0].toString();
          final String fromName    = sources[1].toString();
          final String to          = sources[2].toString();
          final String toName      = sources[3].toString();
          final String subject     = sources[4].toString();
          final String textContent = sources[5].toString();

          try {
            MailHelper.sendSimpleMail(from, fromName, to, toName, null, null, from, subject, textContent);

          } catch (EmailException eex) {
            eex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SEND_PLAINTEXT_MAIL;
      }
    });
    functions.put("send_html_mail", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasMinLengthAndAllElementsNotNull(sources, 6)) {

          final String from        = sources[0].toString();
          final String fromName    = sources[1].toString();
          final String to          = sources[2].toString();
          final String toName      = sources[3].toString();
          final String subject     = sources[4].toString();
          final String htmlContent = sources[5].toString();
          String textContent       = "";

          if (sources.length == 7) {
            textContent = sources[6].toString();
          }

          try {
            MailHelper.sendHtmlMail(from, fromName, to, toName, null, null, from, subject, htmlContent, textContent);

          } catch (EmailException eex) {
            eex.printStackTrace();
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_SEND_HTML_MAIL;
      }
    });
    functions.put("geocode", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (arrayHasLengthAndAllElementsNotNull(sources, 3)) {

          final Gson gson      = new GsonBuilder().create();
          final String street  = sources[0].toString();
          final String city    = sources[1].toString();
          final String country = sources[2].toString();

          GeoCodingResult result = GeoHelper.geocode(street, null, null, city, null, country);
          if (result != null) {

            final Map<String, Object> map = new LinkedHashMap<>();

            map.put("latitude", result.getLatitude());
            map.put("longitude", result.getLongitude());

            return serialize(gson, map);
          }

        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_GEOCODE;
      }
    });
    functions.put("find", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources != null) {

          final SecurityContext securityContext = entity.getSecurityContext();
          final ConfigurationProvider config    = StructrApp.getConfiguration();
          final Query query                     = StructrApp.getInstance(securityContext).nodeQuery();

          // the type to query for
          Class type = null;

          if (sources.length >= 1 && sources[0] != null) {

            type  = config.getNodeEntityClass(sources[0].toString());
            if (type != null) {

              query.andTypes(type);
            }
          }

          switch (sources.length) {

            case 7: // third (key,value) tuple

              final PropertyKey key3 = config.getPropertyKeyForJSONName(type, sources[5].toString());
              if (key3 != null) {

                // throw exception if key is not indexed (otherwise the user will never know)
                if (!key3.isSearchable()) {
                  throw new FrameworkException(400, "Search key " + key3.jsonName() + " is not indexed.");
                }

                final PropertyConverter inputConverter = key3.inputConverter(securityContext);
                Object value                           = sources[6].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                query.and(key3, value);
              }

            case 5: // second (key,value) tuple

              final PropertyKey key2 = config.getPropertyKeyForJSONName(type, sources[3].toString());
              if (key2 != null) {

                // throw exception if key is not indexed (otherwise the user will never know)
                if (!key2.isSearchable()) {
                  throw new FrameworkException(400, "Search key " + key2.jsonName() + " is not indexed.");
                }

                final PropertyConverter inputConverter = key2.inputConverter(securityContext);
                Object value                           = sources[4].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                query.and(key2, value);
              }

            case 3: // (key,value) tuple

              final PropertyKey key1 = config.getPropertyKeyForJSONName(type, sources[1].toString());
              if (key1 != null) {

                // throw exception if key is not indexed (otherwise the user will never know)
                if (!key1.isSearchable()) {
                  throw new FrameworkException(400, "Search key " + key1.jsonName() + " is not indexed.");
                }

                final PropertyConverter inputConverter = key1.inputConverter(securityContext);
                Object value                           = sources[2].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                query.and(key1, value);
              }
              break;
          }

          // return search results
          return query.getAsList();
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_FIND;
      }
    });
    functions.put("create", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources != null) {

          final SecurityContext securityContext = entity.getSecurityContext();
          final App app                         = StructrApp.getInstance(securityContext);
          final ConfigurationProvider config    = StructrApp.getConfiguration();
          PropertyMap propertyMap               = new PropertyMap();

          // the type to query for
          Class type = null;

          if (sources.length >= 1 && sources[0] != null) {


            type  = config.getNodeEntityClass(sources[0].toString());

            if (type.equals(entity.getClass())) {
              throw new FrameworkException(422, "Cannot create() entity of the same type in save action.");
            }
          }

          switch (sources.length) {

            case 7: // third (key,value) tuple

              final PropertyKey key3 = config.getPropertyKeyForJSONName(type, sources[5].toString());
              if (key3 != null) {

                final PropertyConverter inputConverter = key3.inputConverter(securityContext);
                Object value                           = sources[6].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                propertyMap.put(key3, value);
              }

            case 5: // second (key,value) tuple

              final PropertyKey key2 = config.getPropertyKeyForJSONName(type, sources[3].toString());
              if (key2 != null) {

                final PropertyConverter inputConverter = key2.inputConverter(securityContext);
                Object value                           = sources[4].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                propertyMap.put(key2, value);
              }

            case 3: // (key,value) tuple

              final PropertyKey key1 = config.getPropertyKeyForJSONName(type, sources[1].toString());
              if (key1 != null) {

                final PropertyConverter inputConverter = key1.inputConverter(securityContext);
                Object value                           = sources[2].toString();

                if (inputConverter != null) {

                  value = inputConverter.convert(value);
                }

                propertyMap.put(key1, value);
              }
              break;
          }

          if (type != null) {

            return app.create(type, propertyMap);

          } else {

            throw new FrameworkException(422, "Unknown type in create() save action.");
          }

        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_CREATE;
      }
    });
    functions.put("delete", new Function<Object, Object>() {

      @Override
      public Object apply(final ActionContext ctx, final GraphObject entity, final Object[] sources) throws FrameworkException {

        if (sources != null) {

          final App app = StructrApp.getInstance(entity.getSecurityContext());
          for (final Object obj : sources) {

            if (obj instanceof NodeInterface) {

              app.delete((NodeInterface)obj);
              continue;
            }

            if (obj instanceof RelationshipInterface) {

              app.delete((RelationshipInterface)obj);
              continue;
            }
          }
        }

        return "";
      }

      @Override
      public String usage() {
        return ERROR_MESSAGE_DELETE;
      }
    });
  }


  /**
   * Test if the given object array has a minimum length and
   * all its elements are not null.
   *
   * @param array
   * @param minLength If null, don't do length check
   * @return true if array has min length and all elements are not null
   */
  public static boolean arrayHasMinLengthAndAllElementsNotNull(final Object[] array, final Integer minLength) {

    if (array == null) {
      return false;
    }

    for (final Object element : array) {

      if (element == null) {
        return false;
      }

    }

    return minLength != null ? array.length >= minLength : true;

  }

  /**
   * Test if the given object array has exact the given length and
   * all its elements are not null.
   *
   * @param array
   * @param length If null, don't do length check
   * @return true if array has exact length and all elements are not null
   */
  public static boolean arrayHasLengthAndAllElementsNotNull(final Object[] array, final Integer length) {

    if (array == null) {
      return false;
    }

    for (final Object element : array) {

      if (element == null) {
        return false;
      }

    }

    return length != null ? array.length == length : true;

  }

  protected static String serialize(final Gson gson, final Map<String, Object> map) {
    return gson.toJson(map, new TypeToken<Map<String, String>>() { }.getType());
  }

  protected static Map<String, Object> deserialize(final Gson gson, final String source) {
    return gson.fromJson(source, new TypeToken<Map<String, Object>>() { }.getType());
  }

  protected static Integer parseInt(final Object source) {

    if (source instanceof Integer) {

      return ((Integer)source);
    }

    if (source instanceof Number) {

      return ((Number)source).intValue();
    }

    if (source instanceof String) {

      return Integer.parseInt((String)source);
    }

    return null;
  }

  protected static String encodeURL(final String source) {

    try {
      return URLEncoder.encode(source, "UTF-8");

    } catch (UnsupportedEncodingException ex) {

      logger.log(Level.WARNING, "Unsupported Encoding", ex);
    }

    // fallback, unencoded
    return source;
  }

  protected static double getDoubleForComparison(final Object obj) {

    if (obj instanceof Date) {

      return ((Date)obj).getTime();

    } else if (obj instanceof Number) {

      return ((Number)obj).doubleValue();

    } else {

      try {
        return Double.valueOf(obj.toString());

      } catch (Throwable t) {

        t.printStackTrace();
      }
    }

    return 0.0;
  }

  protected static Double getDoubleOrNull(final Object obj) {

    if (obj instanceof Date) {

      return Double.valueOf(((Date)obj).getTime());

    } else if (obj instanceof Number) {

      return ((Number)obj).doubleValue();

    } else {

      try {
        return Double.valueOf(obj.toString());

      } catch (Throwable t) {

        t.printStackTrace();
      }
    }

    return null;
  }

  protected static boolean valueEquals(final Object obj1, final Object obj2) {

    if (obj1 instanceof Enum || obj2 instanceof Enum) {

      return obj1.toString().equals(obj2.toString());

    }

    if (obj1 instanceof Number && obj2 instanceof Number) {

      return ((Number)obj1).doubleValue() == ((Number)obj2).doubleValue();
    }

    return obj1.equals(obj2);
  }

  protected static String getSandboxFileName(final String source) throws IOException {

    final File sandboxFile = new File(source);
    final String fileName  = sandboxFile.getName();
    final String basePath  = StructrApp.getConfigurationValue(Services.BASE_PATH);

    if (!basePath.isEmpty()) {

      final String defaultExchangePath = basePath.endsWith("/") ? basePath.concat("exchange") : basePath.concat("/exchange");
      String exchangeDir               = StructrApp.getConfigurationValue(Services.DATA_EXCHANGE_PATH, defaultExchangePath);

      if (!exchangeDir.endsWith("/")) {
        exchangeDir = exchangeDir.concat("/");
      }

      // create exchange directory
      final File dir = new File(exchangeDir);
      if (!dir.exists()) {

        dir.mkdirs();
      }

      // return sandboxed file name
      return exchangeDir.concat(fileName);


    } else {

      logger.log(Level.WARNING, "Unable to determine base.path from structr.conf, no data input/output possible.");
    }

    return null;
  }

  public static String cleanString(final Object input) {

    if (input == null) {

      return "";

    }

    String normalized = Normalizer.normalize(input.toString(), Normalizer.Form.NFD)
      .replaceAll("\\<", "")
      .replaceAll("\\>", "")
      .replaceAll("\\.", "")
      .replaceAll("\\'", "-")
      .replaceAll("\\?", "")
      .replaceAll("\\(", "")
      .replaceAll("\\)", "")
      .replaceAll("\\{", "")
      .replaceAll("\\}", "")
      .replaceAll("\\[", "")
      .replaceAll("\\]", "")
      .replaceAll("\\+", "-")
      .replaceAll("/", "-")
      .replaceAll("–", "-")
      .replaceAll("\\\\", "-")
      .replaceAll("\\|", "-")
      .replaceAll("'", "-")
      .replaceAll("!", "")
      .replaceAll(",", "")
      .replaceAll("-", " ")
      .replaceAll("_", " ")
      .replaceAll("`", "-");

    String result = normalized.replaceAll("-", " ");
    result = StringUtils.normalizeSpace(result.toLowerCase());
    result = result.replaceAll("[^\\p{ASCII}]", "").replaceAll("\\p{P}", "-").replaceAll("\\-(\\s+\\-)+", "-");
    result = result.replaceAll(" ", "-");

    return result;

  }

  public static void recursivelyConvertMapToGraphObjectMap(final GraphObjectMap target, final Map<String, Object> source, final int depth) {

    if (depth > 20) {
      return;
    }

    for (final Map.Entry<String, Object> entry : source.entrySet()) {

      final String key = entry.getKey();
      final Object value = entry.getValue();

      if (value instanceof Map) {

        final Map<String, Object> map = (Map<String, Object>)value;
        final GraphObjectMap obj      = new GraphObjectMap();

        target.put(new StringProperty(key), obj);

        recursivelyConvertMapToGraphObjectMap(obj, map, depth + 1);

      } else {

        target.put(new StringProperty(key), value);
      }
    }

  }
}
TOP

Related Classes of org.structr.core.parser.Functions

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.