/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2009 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $Id$
*/
package org.exist.xquery.functions.fn;
import org.exist.dom.QName;
import org.exist.xquery.Cardinality;
import org.exist.xquery.Dependency;
import org.exist.xquery.Expression;
import org.exist.xquery.Function;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.Profiler;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.value.FunctionParameterSequenceType;
import org.exist.xquery.value.FunctionReturnSequenceType;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.IntegerValue;
import org.exist.xquery.value.NumericValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.ValueSequence;
/**
* Built-in function fn:substring().
*
* @author Adam Retter <adam.retter@devon.gov.uk>
* @author ljo <ellefj@gmail.com>
*/
public class FunSubstring extends Function {
public final static FunctionSignature signatures[] = {
new FunctionSignature(
new QName("substring", Function.BUILTIN_FUNCTION_NS),
"Returns the portion of the value of $source beginning at the position indicated " +
"by the value of $starting-at and continuing to the end of $source. " +
"The characters returned do not extend beyond the end of $source. If $starting-at " +
"is zero or negative, only those characters in positions greater than zero are returned." +
"If the value of $source is the empty sequence, the zero-length string is returned.",
new SequenceType[] {
new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"),
new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position")
},
new FunctionReturnSequenceType(Type.STRING, Cardinality.ZERO_OR_ONE, "the substring")
),
new FunctionSignature(
new QName("substring", Function.BUILTIN_FUNCTION_NS),
"Returns the portion of the value of $source beginning at the position indicated by the value of $starting-at " +
"and continuing for the number of characters indicated by the value of $length. The characters returned do not extend " +
"beyond the end of $source. If $starting-at is zero or negative, only those characters in positions greater " +
"than zero are returned. If the value of $source is the empty sequence, the zero-length string is returned.",
new SequenceType[] {
new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"),
new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position"),
new FunctionParameterSequenceType("length", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The number of characters in the substring")
},
new FunctionReturnSequenceType(Type.STRING, Cardinality.ZERO_OR_ONE, "the substring")
)
};
public FunSubstring(XQueryContext context, FunctionSignature signature) {
super(context, signature);
}
public int returnsType() {
return Type.STRING;
}
public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException
{
//start profiler
if (context.getProfiler().isEnabled()) {
context.getProfiler().start(this);
context.getProfiler().message(this, Profiler.DEPENDENCIES, "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies()));
if (contextSequence != null)
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT SEQUENCE", contextSequence);}
if (contextItem != null)
{context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());}
}
//get arguments
final Expression argSourceString = getArgument(0);
final Expression argStartingLoc = getArgument(1);
Expression argLength = null;
//get the context sequence
if(contextItem != null)
{contextSequence = contextItem.toSequence();}
Sequence result;
final Sequence seqSourceString = argSourceString.eval(contextSequence);
//If the value of $sourceString is the empty sequence return EMPTY_STRING, there must be a string to operate on!
if(seqSourceString.isEmpty()) {
result = StringValue.EMPTY_STRING;
} else {
//get the string to substring
final String sourceString = seqSourceString.getStringValue();
//check for a valid start position for the substring
final NumericValue startingLoc = ((NumericValue)(argStartingLoc.eval(contextSequence).itemAt(0).convertTo(Type.NUMBER))).round();
if(!validStartPosition(startingLoc, sourceString.length())) {
//invalid start position
result = StringValue.EMPTY_STRING;
} else {
//are there 2 or 3 arguments to this function?
if(getArgumentCount() > 2) {
argLength = getArgument(2);
NumericValue length = new IntegerValue(sourceString.length());
length = ((NumericValue)(argLength.eval(contextSequence).itemAt(0).convertTo(Type.NUMBER))).round();
// Relocate length to position according to spec:
// fn:round($startingLoc) <=
// $p
// < fn:round($startingLoc) + fn:round($length)
NumericValue endingLoc;
if (!length.isInfinite()) {
endingLoc = (NumericValue) new IntegerValue(startingLoc.getInt() + length.getInt());
} else {
endingLoc = length;
}
//check for a valid end position for the substring
if(!validEndPosition(endingLoc, startingLoc)) {
//invalid length
result = StringValue.EMPTY_STRING;
} else {
if(endingLoc.getInt() > sourceString.length()
|| endingLoc.isInfinite()) {
//fallback to fn:substring(string, start)
result = substring(sourceString, startingLoc);
} else {
//three arguments fn:substring(string, start, end)
result = substring(sourceString, startingLoc, endingLoc);
}
}
} else { // No length argument
//two arguments fn:substring(string, start)
result = substring(sourceString, startingLoc);
}
}
}
//end profiler
if (context.getProfiler().isEnabled())
{context.getProfiler().end(this, "", result);}
return result;
}
/**
* Checks that the startPosition is valid for the length of the $sourceString
*
* @param startPosition The user specified startPosition for the fn:substring(), start index is 1
* @param stringLength The length of the $sourceString passed to fn:substring()
*
* @return true if the startPosition is valid, false otherwise
*/
private boolean validStartPosition(NumericValue startPosition, int stringLength)
{
//if start position is not a number return false
if(startPosition.isNaN())
{return false;}
//if start position is infinite return false
if(startPosition.isInfinite())
{return false;}
//if the start position extends beyond $sourceString return EMPTY_STRING
try {
//fn:substring("he",2) must return "e"
if(startPosition.getInt() > stringLength)
{return false;}
} catch(final XPathException xpe) {
return false;
}
//start position is valid
return true;
}
/**
* Checks that the end position is valid for the $sourceString
*
* @param end a <code>NumericValue</code> value
* @param start a <code>NumericValue</code> value
* @return true if the length is valid, false otherwise
* @exception XPathException if an error occurs
*/
private boolean validEndPosition(NumericValue end, NumericValue start)
throws XPathException {
//if end position is not a number
if (end.isNaN())
{return false;}
//if end position is +/-infinite
if (end.isInfinite())
{return true;}
//if end position is less than 1
if (end.getInt() < 1)
{return false;}
//if end position is less than start position
if(end.getInt() <= start.getInt())
{return false;}
//length is valid
return true;
}
/**
* fn:substring($sourceString, $startingLoc)
*
* @see <a href="http://www.w3.org/TR/xpath-functions/#func-substring">w3.org</a>
*
* @param stringSource The source string to substring
* @param startingLoc The Starting Location for the substring, start
* index is 1
*
* @return The StringValue of the substring
*/
private StringValue substring(String sourceString, NumericValue startingLoc)
throws XPathException {
if(startingLoc.getInt() <= 1) {
//start value is 1 or less, so just return the string
return new StringValue(sourceString);
}
final ValueSequence codepoints = FunStringToCodepoints.getCodePoints(sourceString);
// transition from xs:string index to Java string index.
return new StringValue(FunStringToCodepoints.subSequence(codepoints, startingLoc.getInt() - 1));
}
/**
* fn:substring($sourceString, $startingLoc, $length)
*
* @see <a href="http://www.w3.org/TR/xpath-functions/#func-substring">http://www.w3.org/TR/xpath-functions/#func-substring</a>
*
* @param stringSource The source string to substring
* @param startingLoc The Starting Location for the substring, start
* index is 1
* @param length The length of the substring
*
* @return The StringValue of the substring
*/
private StringValue substring(String sourceString, NumericValue startingLoc, NumericValue endingLoc)
throws XPathException {
final ValueSequence codepoints = FunStringToCodepoints.getCodePoints(sourceString);
// transition from xs:string index to Java string index.
return new StringValue(FunStringToCodepoints.subSequence(codepoints,
startingLoc.getInt() - 1,
endingLoc.getInt() - 1));
}
}