/**
* Copyright (C) 2010-2017 Structr GmbH
*
* 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 Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.autocomplete;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
import org.structr.common.PropertyView;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.GraphObjectMap;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.function.Functions;
import org.structr.core.property.GenericProperty;
import org.structr.core.property.IntProperty;
import org.structr.core.property.Property;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.StringProperty;
import org.structr.dynamic.File;
import org.structr.schema.ConfigurationProvider;
import org.structr.schema.SchemaHelper;
import org.structr.schema.action.Function;
import org.structr.schema.action.Hint;
import org.structr.web.entity.User;
import org.structr.web.entity.dom.DOMElement;
import org.structr.web.entity.dom.DOMNode;
import org.structr.web.entity.dom.Page;
import org.structr.web.entity.dom.Template;
public abstract class AbstractHintProvider {
private static final Set<String> startChars = new HashSet<>(Arrays.asList(new String[] { ".", ",", "(", "((", "(((", "((((", "(((((", "((((((", "${" } ));
private static final Set<String> keywords = new HashSet<>();
static {
keywords.add("current");
keywords.add("request");
keywords.add("this");
keywords.add("element");
keywords.add("page");
keywords.add("link");
keywords.add("template");
keywords.add("parent");
keywords.add("children");
keywords.add("host");
keywords.add("port");
keywords.add("path_info");
keywords.add("now");
keywords.add("me");
keywords.add("locale");
}
private enum QueryType {
REST, Cypher, XPath, Function
}
public static final Property<String> displayText = new StringProperty("displayText");
public static final Property<String> text = new StringProperty("text");
public static final Property<GraphObject> from = new GenericProperty("from");
public static final Property<GraphObject> to = new GenericProperty("to");
public static final Property<Integer> line = new IntProperty("line");
public static final Property<Integer> ch = new IntProperty("ch");
/**
* Allowes the implementer to transform the given sourceName
* according to its own rules.
*
* @param sourceName
* @return the transformed sourceName
*/
protected abstract String getFunctionName(final String sourceName);
protected abstract boolean isJavascript();
private final Comparator comparator = new HintComparator();
public List<GraphObject> getHints(final GraphObject currentEntity, final String type, final String currentToken, final String previousToken, final String thirdToken, final int cursorLine, final int cursorPosition) {
final List<Hint> allHints = getAllHints(currentEntity, currentToken, previousToken, thirdToken);
final List<GraphObject> hints = new LinkedList<>();
int maxNameLength = 0;
if (StringUtils.isBlank(currentToken) || startChars.contains(currentToken)) {
// display all possible hints
for (final Hint hint : allHints) {
final GraphObjectMap item = new GraphObjectMap();
String functionName = getFunctionName(hint.getReplacement());
if (hint.mayModify()) {
item.put(text, visitReplacement(functionName));
} else {
item.put(text, functionName);
}
item.put(displayText, getFunctionName(hint.getName()) + " - " + textOrPlaceholder(hint.shortDescription()));
addPosition(item, hint, cursorLine, cursorPosition, cursorPosition);
if (functionName.length() > maxNameLength) {
maxNameLength = functionName.length();
}
hints.add(item);
}
} else {
final int currentTokenLength = currentToken.length();
for (final Hint hint : allHints) {
final String functionName = getFunctionName(hint.getName());
final String replacement = hint.getReplacement();
if (functionName.startsWith(currentToken) || (currentToken.length() > 2 && functionName.contains(currentToken))) {
final GraphObjectMap item = new GraphObjectMap();
if (hint.mayModify()) {
item.put(text, visitReplacement(replacement));
} else {
item.put(text, replacement);
}
item.put(displayText, getFunctionName(hint.getName()) + " - " + textOrPlaceholder(hint.shortDescription()));
addPosition(item, hint, cursorLine, cursorPosition - currentTokenLength, cursorPosition);
if (functionName.length() > maxNameLength) {
maxNameLength = functionName.length();
}
hints.add(item);
}
}
}
alignHintDescriptions(hints, maxNameLength);
return hints;
}
protected String visitReplacement(final String replacement) {
return replacement;
}
protected Hint createHint(final String name, final String signature, final String description) {
return createHint(name, signature, description, null);
}
protected Hint createHint(final String name, final String signature, final String description, final String replacement) {
final NonFunctionHint hint = new NonFunctionHint() {
@Override
public String shortDescription() {
return description;
}
@Override
public String getName() {
return name;
}
};
if (replacement != null) {
hint.setReplacement(replacement);
}
return hint;
}
protected void alignHintDescriptions(final List<GraphObject> hints, final int maxNameLength) {
// insert appropriate number of spaces into description to align function names
for (final GraphObject item : hints) {
final String text = item.getProperty(displayText);
final int pos = text.indexOf(" - ");
if (pos < maxNameLength) {
final StringBuilder buf = new StringBuilder(text);
buf.insert(pos, StringUtils.leftPad("", maxNameLength - pos));
// ignore exception, won't happen on a GraphObjectMap anyway
try { item.setProperty(displayText, buf.toString()); } catch (FrameworkException fex) {}
}
}
}
protected String textOrPlaceholder(final String source) {
if (StringUtils.isBlank(source)) {
return " (no description available yet)";
}
return source;
}
protected List<Hint> getAllHints(final GraphObject currentNode, final String currentToken, final String previousToken, final String thirdToken) {
final boolean isDeclaration = isJavascript() && "var".equals(previousToken);
final boolean isAssignment = isJavascript() && "=".equals(previousToken);
final boolean isDotNotationRequest = ".".equals(currentToken);
final ConfigurationProvider config = StructrApp.getConfiguration();
final Map<String, DataKey> dataKeys = new TreeMap<>();
final List<Hint> hints = new LinkedList<>();
final List<Hint> local = new LinkedList<>();
Class currentObjectType = null;
// data key etc. hints
if (currentNode != null) {
recursivelyFindDataKeys(currentNode, dataKeys);
}
switch (previousToken) {
case "current":
currentObjectType = AbstractNode.class;
break;
case "this":
currentObjectType = DOMNode.class;
break;
case "me":
currentObjectType = User.class;
break;
case "page":
currentObjectType = Page.class;
break;
case "link":
currentObjectType = File.class;
break;
case "template":
currentObjectType = Template.class;
break;
case "parent":
currentObjectType = DOMElement.class;
break;
default:
DataKey key = dataKeys.get(previousToken);
if (key != null) {
currentObjectType = key.identifyType(config);
} else if (StringUtils.isNotBlank(thirdToken)) {
key = dataKeys.get(thirdToken);
if (key != null) {
currentObjectType = key.identifyType(config);
if (currentObjectType != null) {
final PropertyKey nestedKey = config.getPropertyKeyForJSONName(currentObjectType, previousToken, false);
if (nestedKey != null) {
currentObjectType = nestedKey.relatedType();
}
}
}
}
break;
}
if (!keywords.contains(previousToken) && !isDotNotationRequest && !dataKeys.containsKey(previousToken)) {
if (!isAssignment) {
for (final Function<Object, Object> func : Functions.functions.values()) {
hints.add(func);
}
}
Collections.sort(hints, comparator);
// non-function hints
local.add(createHint("current", "", "Current data object", !isJavascript() ? null : "get('current')"));
local.add(createHint("request", "", "Current request object", !isJavascript() ? null : "get('request')"));
local.add(createHint("this", "", "Current object", !isJavascript() ? null : "get('this')"));
local.add(createHint("element", "", "Current object", !isJavascript() ? null : "get('element')"));
local.add(createHint("page", "", "Current page", !isJavascript() ? null : "get('page')"));
local.add(createHint("link", "", "Current link", !isJavascript() ? null : "get('link')"));
local.add(createHint("template", "", "Closest template node", !isJavascript() ? null : "get('template')"));
local.add(createHint("parent", "", "Parent node", !isJavascript() ? null : "get('parent')"));
local.add(createHint("children", "", "Collection of child nodes", !isJavascript() ? null : "get('children')"));
local.add(createHint("host", "", "Client's host name", !isJavascript() ? null : "get('host')"));
local.add(createHint("port", "", "Client's port", !isJavascript() ? null : "get('port')"));
local.add(createHint("path_info", "", "URL path", !isJavascript() ? null : "get('path_info')"));
local.add(createHint("now", "", "Current date", !isJavascript() ? null : "get('now')"));
local.add(createHint("me", "", "Current user", !isJavascript() ? null : "get('me)"));
local.add(createHint("locale", "", "Current locale", !isJavascript() ? null : "get('locale')"));
}
// add local hints to the beginning of the list
Collections.sort(local, comparator);
hints.addAll(0, local);
// prepend data keys
if (currentObjectType == null && !dataKeys.containsKey(previousToken) && !isDotNotationRequest || isAssignment) {
for (final DataKey dataKey : dataKeys.values()) {
final String replacement = isJavascript() && !isDeclaration ? "get('" + dataKey.getDataKey() + "')" : null;
final Hint dataKeyHint = createHint(dataKey.getDataKey(), "", dataKey.getDescription(), replacement);
// disable replacement with "Structr.get(...)" when in Javascript declaration
dataKeyHint.allowNameModification(!isDeclaration);
hints.add(0, dataKeyHint);
}
}
// prepend property keys of current object type
collectHintsForType(hints, config, currentObjectType);
return hints;
}
// ----- private methods -----
private void addPosition(final GraphObjectMap item, final Hint hint, final int cursorLine, final int replaceFrom, final int replaceTo) {
final GraphObjectMap fromObject = new GraphObjectMap();
final GraphObjectMap toObject = new GraphObjectMap();
fromObject.put(line, cursorLine);
fromObject.put(ch, replaceFrom);
toObject.put(line, cursorLine);
toObject.put(ch, replaceTo);
item.put(from, fromObject);
item.put(to, toObject);
}
private void recursivelyFindDataKeys(final GraphObject entity, final Map<String, DataKey> dataKeys) {
if (entity != null) {
final String dataKey = entity.getProperty(DOMNode.dataKey);
if (dataKey != null) {
final DataKey key = new DataKey(entity);
dataKeys.put(key.getDataKey(), key);
}
recursivelyFindDataKeys(entity.getProperty(DOMNode.parent), dataKeys);
}
}
private void collectHintsForType(final List<Hint> hints, final ConfigurationProvider config, final Class type) {
if (type != null) {
final List<Hint> propertyHints = new LinkedList<>();
// create hints based on schema type information
for (final PropertyKey propertyKey : config.getPropertySet(type, PropertyView.All)) {
final String keyName = propertyKey.jsonName();
if (!keyName.startsWith(PropertyView.Html) && !keyName.startsWith("data-structr-")) {
final Hint propertyHint = createHint(keyName, "", type.getSimpleName() + " property");
// allow sorting by dynamic / static properties
propertyHint.setIsDynamic(propertyKey.isDynamic());
propertyHint.allowNameModification(false);
propertyHints.add(propertyHint);
}
}
Collections.sort(propertyHints, comparator);
hints.addAll(0, propertyHints);
}
}
// ----- nested classes -----
protected static class HintComparator implements Comparator<Hint> {
@Override
public int compare(final Hint o1, final Hint o2) {
final boolean firstIsDynamic = o1.isDynamic();
final boolean secindIsDynamic = o2.isDynamic();
if (firstIsDynamic && !secindIsDynamic) {
return -1;
}
if (!firstIsDynamic && secindIsDynamic) {
return 1;
}
return o1.getName().compareTo(o2.getName());
}
}
private static class DataKey implements Comparable<DataKey> {
private QueryType queryType = QueryType.REST;
private String dataKey = null;
private String query = null;
public DataKey(final GraphObject entity) {
dataKey = entity.getProperty(DOMNode.dataKey);
query = entity.getProperty(DOMNode.restQuery);
if (query == null) {
query = entity.getProperty(DOMNode.cypherQuery);
queryType = QueryType.Cypher;
}
if (query == null) {
query = entity.getProperty(DOMNode.xpathQuery);
queryType = QueryType.XPath;
}
if (query == null) {
query = entity.getProperty(DOMNode.functionQuery);
queryType = QueryType.Function;
}
}
public String getDataKey() {
return dataKey;
}
public String getDescription() {
final StringBuilder buf = new StringBuilder();
buf.append("Data key for ");
buf.append(queryType);
buf.append(" query ");
buf.append(StringUtils.abbreviate(query, 20));
return buf.toString();
}
@Override
public int compareTo(final DataKey other) {
return dataKey.compareTo(other.getDataKey());
}
public Class identifyType(final ConfigurationProvider config) {
// only for REST right now
switch (queryType) {
case REST:
return identifyRestQueryType();
case Cypher:
break;
case XPath:
// the result is very likely to be a DOMNode
return DOMNode.class;
case Function:
break;
}
return null;
}
private Class identifyRestQueryType() {
// remove template expressions
String cleanedQuery = query.replaceAll("\\$\\{.*\\}", "");
// remove optional / for REST
if (cleanedQuery.startsWith("/")) {
cleanedQuery = cleanedQuery.substring(1);
}
final int queryStart = cleanedQuery.indexOf("?");
if (queryStart >= 0 && queryStart < cleanedQuery.length()) {
cleanedQuery = cleanedQuery.substring(0, queryStart);
}
return SchemaHelper.getEntityClassForRawType(cleanedQuery);
}
}
}