blob: 40622c8d5d4f091de462a22a1e6da0d9e7d06aad [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.jackrabbit.oak.plugins.index.search.util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.memory.EmptyPropertyState;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.query.QueryConstants;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A parser for function-based indexes. It converts the human-readable function
* definition (XPath) to the internal Polish notation.
*/
public class FunctionIndexProcessor {
private static final Logger LOG =
LoggerFactory.getLogger(FunctionIndexProcessor.class);
private String remaining;
private static final PropertyState EMPTY_PROPERTY_STATE = EmptyPropertyState.emptyProperty("empty", Type.STRINGS);
protected FunctionIndexProcessor(String function) {
this.remaining = function;
}
/**
* Get the list of properties used in the given function code.
*
* @param functionCode the tokens, for example ["function", "lower", "@name"]
* @return the list of properties, for example ["name"]
*/
public static String[] getProperties(String[] functionCode) {
ArrayList<String> properties = new ArrayList<>();
for(String token : functionCode) {
if (token.startsWith("@")) {
String propertyName = token.substring(1);
properties.add(propertyName);
}
}
return properties.toArray(new String[0]);
}
/**
* Try to calculate the value for the given function code.
*
* @param path the path of the node
* @param state the node state
* @param functionCode the tokens, for example ["function", "lower", "@name"]
* @return null, or the calculated value
*/
public static PropertyState tryCalculateValue(String path, NodeState state, String[] functionCode) {
Deque<PropertyState> stack = new ArrayDeque<>();
for (int i = functionCode.length - 1; i > 0; i--) {
String token = functionCode[i];
PropertyState ps;
if (token.startsWith("@")) {
String propertyName = token.substring(1);
ps = getProperty(path, state, propertyName);
} else {
ps = calculateFunction(token, stack);
}
if (ps == null) {
ps = EMPTY_PROPERTY_STATE;
}
stack.push(ps);
}
PropertyState ret = stack.pop();
return ret==EMPTY_PROPERTY_STATE ? null : ret;
}
/**
* Split the polish notation into a tokens that can more easily be processed.
*
* @param functionDescription in polish notation, for example "function*lower*{@literal @}name"
* @return tokens, for example ["function", "lower", "{@literal @}name"]
*/
public static String[] getFunctionCode(String functionDescription) {
if (functionDescription == null) {
return null;
}
return functionDescription.split("\\*");
}
private static PropertyState calculateFunction(String functionName,
Deque<PropertyState> stack) {
PropertyState ps = stack.pop();
if ("coalesce".equals(functionName)) {
// coalesce (a, b) => (a != null ? a : b)
// we pop stack again to consume the second parameter
// also, if ps is EMPTY_PROPERTY_STATE, then newly popped value is to be used
PropertyState ps2 = stack.pop();
if (ps == EMPTY_PROPERTY_STATE) {
ps = ps2;
}
}
if (ps == EMPTY_PROPERTY_STATE) {
return ps;
}
Type<?> type = null;
ArrayList<Object> values = new ArrayList<>(ps.count());
for (int i = 0; i < ps.count(); i++) {
String s = ps.getValue(Type.STRING, i);
Object x;
if ("lower".equals(functionName)) {
x = s.toLowerCase();
type = Type.STRING;
} else if ("coalesce".equals(functionName)) {
x = s;
type = Type.STRING;
} else if ("upper".equals(functionName)) {
x = s.toUpperCase();
type = Type.STRING;
} else if ("length".equals(functionName)) {
x = (long) s.length();
type = Type.LONG;
} else {
LOG.debug("Unknown function {}", functionName);
return null;
}
values.add(x);
}
PropertyState result;
if (values.size() == 1) {
result = PropertyStates.createProperty("value", values.get(0), type);
} else {
type = type.getArrayType();
result = PropertyStates.createProperty("value", values, type);
}
return result;
}
private static PropertyState getProperty(String path, NodeState state,
String propertyName) {
if (PathUtils.getDepth(propertyName) != 1) {
for(String n : PathUtils.elements(PathUtils.getParentPath(propertyName))) {
state = state.getChildNode(n);
if (!state.exists()) {
return null;
}
}
propertyName = PathUtils.getName(propertyName);
}
PropertyState ps;
if (":localname".equals(propertyName)) {
ps = PropertyStates.createProperty("value",
getLocalName(PathUtils.getName(path)), Type.STRING);
} else if (":name".equals(propertyName)) {
ps = PropertyStates.createProperty("value",
PathUtils.getName(path), Type.STRING);
} else {
ps = state.getProperty(propertyName);
}
if (ps == null || ps.count() == 0) {
return null;
}
return ps;
}
private static String getLocalName(String name) {
int colon = name.indexOf(':');
// TODO LOCALNAME: evaluation of local name might not be correct
return colon < 0 ? name : name.substring(colon + 1);
}
/**
* Convert a function (in human-readable form) to the polish notation.
*
* @param function the function, for example "lower([name])"
* @return the polish notation, for example "function*lower*{@literal @}name"
*/
public static String convertToPolishNotation(String function) {
if (function == null) {
return null;
}
FunctionIndexProcessor p = new FunctionIndexProcessor(function);
return QueryConstants.FUNCTION_RESTRICTION_PREFIX + p.parse();
}
String parse() {
if (match("fn:local-name()") || match("localname()")) {
return "@:localname";
}
if (match("fn:name()") || match("name()")) {
return "@:name";
}
if (match("fn:upper-case(") || match("upper(")) {
return "upper*" + parse() + read(")");
}
if (match("fn:lower-case(") || match("lower(")) {
return "lower*" + parse() + read(")");
}
if (match("fn:coalesce(") || match("coalesce(")) {
return "coalesce*" + parse() + readCommaAndWhitespace() + parse() + read(")");
}
if (match("fn:string-length(") || match("length(")) {
return "length*" + parse() + read(")");
}
// property name
if (match("[")) {
String prop = remaining;
int indexOfComma = remaining.indexOf(",");
if (indexOfComma > 0) {
prop = remaining.substring(0, indexOfComma);
}
prop = prop.substring(0, prop.lastIndexOf(']'));
remaining = remaining.substring(prop.length() + 1);
return property(prop.replaceAll("]]", "]"));
} else {
String prop = remaining;
int paren = remaining.indexOf(')');
int comma = remaining.indexOf(',');
int end = comma;
if (paren >=0) {
end = (end < 0) ? paren : Math.min(end, paren);
}
if (end >= 0) {
prop = remaining.substring(0, end);
}
remaining = remaining.substring(prop.length());
return property(prop.replaceAll("@", ""));
}
}
String property(String p) {
return "@" + p;
}
private String read(String string) {
match(string);
return "";
}
private String
readCommaAndWhitespace() {
while (match(" ")) {
}
match(",");
while (match(" ")) {
}
return "*";
}
private boolean match(String string) {
if (remaining.startsWith(string)) {
remaining = remaining.substring(string.length());
return true;
}
return false;
}
}