/*
* Copyright 2009 DuraSpace.
*
* Licensed 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.mulgara.query.xpath;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.xpath.XPathFunctionException;
import org.apache.xerces.impl.xpath.regex.RegularExpression;
import org.mulgara.query.functions.MulgaraFunction;
import org.mulgara.query.functions.MulgaraFunctionGroup;
import org.mulgara.util.NumberUtil;
/**
* Container for functions in the fn domain.
*
* @created Oct 5, 2009
* @author Paula Gearon
* @copyright © 2009 <a href="http://www.duraspace.org/">DuraSpace</a>
*/
public class FnFunctionGroup implements MulgaraFunctionGroup {
/** The prefix for the fn: namespace */
static final String PREFIX = "fn";
/** The fn: namespace */
static final String NAMESPACE = "http://www.w3.org/2005/xpath-functions/#";
/** The name of the UTF-8 encoding scheme */
static private final String UTF8 = "UTF-8";
/**
* Get the prefix used for the namespace of these operations.
* @return The short string used for a prefix in a QName.
*/
public String getPrefix() {
return PREFIX;
}
/**
* Get the namespace of these operations.
* @return The string of the namespace URI.
*/
public String getNamespace() {
return NAMESPACE;
}
/**
* Get the set of SPARQL functions.
* @return A set of MulgaraFunction for this entire group.
*/
public Set<MulgaraFunction> getAllFunctions() {
Set<MulgaraFunction> functions = new HashSet<MulgaraFunction>();
functions.add(new Matches2());
functions.add(new Matches3());
functions.add(new FnBoolean());
functions.add(new Not());
functions.add(new Concat());
functions.add(new Substring2());
functions.add(new Substring3());
functions.add(new StringLength());
functions.add(new NormalizeSpace());
functions.add(new UpperCase());
functions.add(new LowerCase());
functions.add(new Translate());
functions.add(new EncodeForUri());
functions.add(new IriToUri());
functions.add(new EscapeHtmlUri());
functions.add(new Contains());
functions.add(new StartsWith());
functions.add(new EndsWith());
functions.add(new StringJoin());
functions.add(new Round());
functions.add(new Abs());
functions.add(new Floor());
functions.add(new Ceiling());
return functions;
}
/**
* Function to evaluate if a string matches a pattern.
* @see http://www.w3.org/TR/xpath-functions/#func-matches
*/
static private class Matches2 extends MulgaraFunction {
public String getName() { return "matches/2"; }
public int getArity() { return 2; }
public Object eval(List<?> args) {
String str = (String)args.get(0);
String pattern = (String)args.get(1);
return new RegularExpression(pattern).matches(str);
}
}
/**
* Function to evaluate if a string matches a pattern, with a set of modifying flags.
* @see http://www.w3.org/TR/xpath-functions/#func-matches
*/
static private class Matches3 extends MulgaraFunction {
public String getName() { return "matches/3"; }
public int getArity() { return 3; }
public Object eval(List<?> args) {
String str = (String)args.get(0);
String pattern = (String)args.get(1);
String flags = (String)args.get(2);
return new RegularExpression(pattern, flags).matches(str);
}
}
/**
* Function to compute the EBV of a sequence. Unfortunately, no sequence info is available at this level.
* @see http://www.w3.org/TR/xpath-functions/#func-boolean
*/
static private class FnBoolean extends MulgaraFunction {
public String getName() { return "boolean"; }
public int getArity() { return 1; }
public Object eval(List<?> args) throws XPathFunctionException {
// no sequence info available here. Look at singleton only for the moment
return toBool(args.get(0));
}
}
// No implementation of fn:compare
/**
* Function to compute the inverse EBV of a sequence. See FnBoolean for restrictions.
* @see http://www.w3.org/TR/xpath-functions/#func-not
*/
static private class Not extends MulgaraFunction {
public int getArity() { return 1; }
public Object eval(List<?> args) throws XPathFunctionException {
return !toBool(args.get(0));
}
}
/**
* Concatenates two or more arguments cast to string.
* @see http://www.w3.org/TR/xpath-functions/#func-concat
*/
static private class Concat extends MulgaraFunction {
public int getArity() { return -1; }
public String getName() { return "concat/*"; }
public Object eval(List<?> args) throws XPathFunctionException {
StringBuilder s = new StringBuilder();
for (Object o: args) s.append(o);
return s.toString();
}
}
/**
* Returns the string located at a specified place within an argument string.
* @see http://www.w3.org/TR/xpath-functions/#func-substring
*/
static private class Substring2 extends MulgaraFunction {
public int getArity() { return 2; }
public String getName() { return "substring/2"; }
public Object eval(List<?> args) throws XPathFunctionException {
String source = args.get(0).toString();
int start = ((Number)args.get(1)).intValue() - 1;
// perform boundary checking
int len = source.length();
if (start < 0) start = 0;
if (start > len) start = len;
return source.substring(start);
}
}
/**
* Returns the string located at a specified place within an argument string.
* @see http://www.w3.org/TR/xpath-functions/#func-substring
*/
static private class Substring3 extends MulgaraFunction {
public int getArity() { return 3; }
public String getName() { return "substring/3"; }
public Object eval(List<?> args) throws XPathFunctionException {
String source = args.get(0).toString();
int start = ((Number)args.get(1)).intValue() - 1;
int end = ((Number)args.get(2)).intValue() + start;
// perform boundary checking
int len = source.length();
if (start < 0) start = 0;
if (start > len) {
start = len;
end = len;
}
if (end > len) end = len;
if (end < start) end = start;
return source.substring(start, end);
}
}
/**
* Returns the length of the argument.
* @see http://www.w3.org/TR/xpath-functions/#func-string-length
*/
static private class StringLength extends MulgaraFunction {
public String getName() { return "string-length/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
return args.get(0).toString().length();
}
}
/**
* Returns the whitespace-normalized value of the argument.
* @see http://www.w3.org/TR/xpath-functions/#func-normalize-space
*/
static private class NormalizeSpace extends MulgaraFunction {
public String getName() { return "normalize-space/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
String str = args.get(0).toString().trim();
return str.replaceAll(" +", " ");
}
}
/**
* Returns the upper-cased value of the argument.
* @see http://www.w3.org/TR/xpath-functions/#func-upper-case
*/
static private class UpperCase extends MulgaraFunction {
public String getName() { return "upper-case/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
return args.get(0).toString().toUpperCase();
}
}
/**
* Returns the lower-cased value of the argument.
* @see http://www.w3.org/TR/xpath-functions/#func-lower-case
*/
static private class LowerCase extends MulgaraFunction {
public String getName() { return "lower-case/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
return args.get(0).toString().toLowerCase();
}
}
/**
* Returns the first xs:string argument with occurrences of characters contained
* in the second argument replaced by the character at the corresponding position
* in the third argument.
*/
static private class Translate extends MulgaraFunction {
public int getArity() { return 3; }
public Object eval(List<?> args) throws XPathFunctionException {
String str = args.get(0).toString();
String mapStr = args.get(1).toString();
String transStr = args.get(2).toString();
// iterate through the map chars
for (int i = 0; i < mapStr.length(); i++) {
char c = mapStr.charAt(i);
if (i < transStr.length()) str = replaceChars(str, c, transStr.charAt(i));
else str = removeChars(str, c);
}
return str;
}
private static String replaceChars(String str, char c, char r) {
StringBuilder s = new StringBuilder(str);
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) s.setCharAt(i, r);
}
return s.toString();
}
private static String removeChars(String str, char c) {
StringBuilder s = new StringBuilder(str);
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) {
s.replace(i, i + 1, "");
i--;
}
}
return s.toString();
}
}
/**
* Returns the xs:string argument with certain characters escaped to enable the
* resulting string to be used as a path segment in a URI.
* @see http://www.w3.org/TR/xpath-functions/#func-encode-for-uri
*/
static private class EncodeForUri extends MulgaraFunction {
public String getName() { return "encode-for-uri/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
try {
return URLEncoder.encode(args.get(0).toString(), UTF8);
} catch (UnsupportedEncodingException e) {
throw new XPathFunctionException("Unable to encode string for URL: " + e.getMessage());
}
}
}
/**
* Returns the xs:string argument with certain characters escaped to enable the
* resulting string to be used as (part of) a URI.
* @see http://www.w3.org/TR/xpath-functions/#func-iri-to-uri
*/
static private class IriToUri extends MulgaraFunction {
public String getName() { return "iri-to-uri/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
StringBuilder str = new StringBuilder(args.get(0).toString());
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (outOfRange(c)) str.replace(i, i + 1, escape(str.substring(i, i + 1)));
}
return str.toString();
}
/**
* Test for URI compatibility. See Errata note:
* @see http://www.w3.org/XML/2007/qt-errata/xpath-functions-errata.html#E8
* @param c The character to test.
* @return <code>true</code> if the character is out of range for a URI and must be encoded.
*/
static private final boolean outOfRange(char c) {
return c < 0x20 || c > 0x7E || c == '<' || c == '>' || c == '"' ||
c == '{' || c == '}' || c == '|' || c == '\\' || c == '^' || c == '`';
}
}
/**
* Returns the string argument with certain characters escaped in the manner that html user agents
* handle attribute values that expect URIs.
* @see http://www.w3.org/TR/xpath-functions/#func-escape-html-uri
*/
static private class EscapeHtmlUri extends MulgaraFunction {
public String getName() { return "escape-html-uri/1"; }
public Object eval(List<?> args) throws XPathFunctionException {
StringBuilder str = new StringBuilder(args.get(0).toString());
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (outOfRange(c)) str.replace(i, i + 1, escape(str.substring(i, i + 1)));
}
return str.toString();
}
static private final boolean outOfRange(char c) {
return c < 0x20 || c > 0x7E;
}
}
/**
* Converts a single character to a string containing escaped UTF-8 encoding.
* A utility used by both EscapeHtmlUri and IriToUri.
* @param c The character to escape.
* @return A string containing a sequence of %HH representing the UTF-8 encoding of <var>c</var>
* @throws XPathFunctionException If UTF-8 encoding fails.
*/
static private final String escape(String c) throws XPathFunctionException {
byte[] bytes = null;
try {
bytes = c.getBytes(UTF8);
} catch (UnsupportedEncodingException e) {
throw new XPathFunctionException("Unable to encode string for URL: " + e.getMessage());
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < bytes.length; i++) result.append(String.format("%%%02X", bytes[i]));
return result.toString();
}
/**
* Test whether a substring occurs in a string
* fn:contains(string,substr)
* @see http://www.w3.org/TR/xpath-functions/#contains
*/
static private class Contains extends MulgaraFunction {
public int getArity() { return 2; }
public Object eval(List<?> args) {
String str = (String)args.get(0);
String substr = (String)args.get(1);
return str.contains(substr);
}
}
/**
* Test whether a string starts with substr
* fn:starts-with(string,substr)
* @see http://www.w3.org/TR/xpath-functions/#starts-with
*/
static private class StartsWith extends MulgaraFunction {
public String getName() { return "starts-with/2"; }
public int getArity() { return 2; }
public Object eval(List<?> args) {
String str = (String)args.get(0);
String substr = (String)args.get(1);
return str.startsWith(substr);
}
}
/**
* Test whether a string ends with substr
* fn:ends-with(string,substr)
* @see http://www.w3.org/TR/xpath-functions/#ends-with
*/
static private class EndsWith extends MulgaraFunction {
public String getName() { return "ends-with/2"; }
public int getArity() { return 2; }
public Object eval(List<?> args) {
String str = (String)args.get(0);
String substr = (String)args.get(1);
return str.endsWith(substr);
}
}
/**
* Join all the arguments except the last, using the last argument as a separator.
* fn:string-join(sequence..., separator)
* @see http://www.w3.org/TR/xpath-functions/#string-join
*/
static private class StringJoin extends MulgaraFunction {
public String getName() { return "string-join/*"; }
public int getArity() { return -1; }
public Object eval(List<?> args) throws XPathFunctionException {
StringBuilder s = new StringBuilder();
int lastIndex = args.size() - 1;
String separator = (String)args.get(lastIndex);
for (int i = 0; i < lastIndex; i++) {
if (i != 0) s.append(separator);
s.append(args.get(i));
}
return s.toString();
}
}
/**
* Return the nearest integer value to the argument.
* fn:round(x)
* @see http://www.w3.org/TR/xpath-functions/#round
*/
static private class Round extends MulgaraFunction {
public Object eval(List<?> args) throws XPathFunctionException {
Number x = (Number)args.get(0);
return Math.round(x.doubleValue());
}
}
/**
* Return the absolute value.
* fn:abs(x)
* @see http://www.w3.org/TR/xpath-functions/#abs
*/
static private class Abs extends MulgaraFunction {
public Object eval(List<?> args) throws XPathFunctionException {
Number x = (Number)args.get(0);
return x.doubleValue() < 0 ? NumberUtil.minus(x) : x;
}
}
/**
* Return the greatest integer value less than the argument (as a double).
* fn:floor(x)
* @see http://www.w3.org/TR/xpath-functions/#floor
*/
static private class Floor extends MulgaraFunction {
public Object eval(List<?> args) throws XPathFunctionException {
Number x = (Number)args.get(0);
return Math.floor(x.doubleValue());
}
}
/**
* Return the smallest integer value greater than the argument (as a double).
* fn:ceiling(x)
* @see http://www.w3.org/TR/xpath-functions/#ceiling
*/
static private class Ceiling extends MulgaraFunction {
public Object eval(List<?> args) throws XPathFunctionException {
Number x = (Number)args.get(0);
return Math.ceil(x.doubleValue());
}
}
}