blob: 87f64065170443460c1463a53985bafa933628e6 [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.
*/
/* $Id$ */
package org.apache.fop.fo.expr;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import org.apache.xmlgraphics.util.UnitConv;
import org.apache.fop.datatypes.Length;
import org.apache.fop.datatypes.LengthBase;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.datatypes.PercentBase;
import org.apache.fop.fo.properties.ColorProperty;
import org.apache.fop.fo.properties.FixedLength;
import org.apache.fop.fo.properties.ListProperty;
import org.apache.fop.fo.properties.NumberProperty;
import org.apache.fop.fo.properties.PercentLength;
import org.apache.fop.fo.properties.Property;
import org.apache.fop.fo.properties.StringProperty;
/**
* Class to parse XSL-FO property expressions.
* This class is heavily based on the epxression parser in James Clark's
* XT, an XSLT processor.
*/
public final class PropertyParser extends PropertyTokenizer {
private PropertyInfo propInfo; // Maker and propertyList related info
private static final String RELUNIT = "em";
private static final HashMap FUNCTION_TABLE = new HashMap();
static {
// Initialize the HashMap of XSL-defined functions
FUNCTION_TABLE.put("ceiling", new CeilingFunction());
FUNCTION_TABLE.put("floor", new FloorFunction());
FUNCTION_TABLE.put("round", new RoundFunction());
FUNCTION_TABLE.put("min", new MinFunction());
FUNCTION_TABLE.put("max", new MaxFunction());
FUNCTION_TABLE.put("abs", new AbsFunction());
FUNCTION_TABLE.put("rgb", new RGBColorFunction());
FUNCTION_TABLE.put("system-color", new SystemColorFunction());
FUNCTION_TABLE.put("from-table-column", new FromTableColumnFunction());
FUNCTION_TABLE.put("inherited-property-value",
new InheritedPropFunction());
FUNCTION_TABLE.put("from-parent", new FromParentFunction());
FUNCTION_TABLE.put("from-nearest-specified-value",
new NearestSpecPropFunction());
FUNCTION_TABLE.put("proportional-column-width",
new PPColWidthFunction());
FUNCTION_TABLE.put("label-end", new LabelEndFunction());
FUNCTION_TABLE.put("body-start", new BodyStartFunction());
FUNCTION_TABLE.put("rgb-icc", new ICCColorFunction());
FUNCTION_TABLE.put("cmyk", new CMYKcolorFunction()); //non-standard!!!
/**
* * NOT YET IMPLEMENTED!!!
* FUNCTION_TABLE.put("system-font", new SystemFontFunction());
* FUNCTION_TABLE.put("merge-property-values", new MergePropsFunction());
*/
}
/**
* Public entrypoint to the Property expression parser.
* @param expr The specified value (attribute on the xml element).
* @param propInfo A PropertyInfo object representing the context in
* which the property expression is to be evaluated.
* @return A Property object holding the parsed result.
* @throws PropertyException If the "expr" cannot be parsed as a Property.
*/
public static Property parse(String expr, PropertyInfo propInfo)
throws PropertyException {
try {
return new PropertyParser(expr, propInfo).parseProperty();
} catch (PropertyException exc) {
exc.setPropertyInfo(propInfo);
throw exc;
}
}
/**
* Private constructor. Called by the static parse() method.
* @param propExpr The specified value (attribute on the xml element).
* @param pInfo A PropertyInfo object representing the context in
* which the property expression is to be evaluated.
*/
private PropertyParser(String propExpr, PropertyInfo pInfo) {
super(propExpr);
this.propInfo = pInfo;
}
/**
* Parse the property expression described in the instance variables.
* Note: If the property expression String is empty, a StringProperty
* object holding an empty String is returned.
* @return A Property object holding the parsed result.
* @throws PropertyException If the "expr" cannot be parsed as a Property.
*/
private Property parseProperty() throws PropertyException {
next();
if (currentToken == TOK_EOF) {
// if prop value is empty string, force to StringProperty
return StringProperty.getInstance("");
}
ListProperty propList = null;
while (true) {
Property prop = parseAdditiveExpr();
if (currentToken == TOK_EOF) {
if (propList != null) {
propList.addProperty(prop);
return propList;
} else {
return prop;
}
} else {
if (propList == null) {
propList = new ListProperty(prop);
} else {
propList.addProperty(prop);
}
}
// throw new PropertyException("unexpected token");
}
// return prop;
}
/**
* Try to parse an addition or subtraction expression and return the
* resulting Property.
*/
private Property parseAdditiveExpr() throws PropertyException {
// Evaluate and put result on the operand stack
Property prop = parseMultiplicativeExpr();
loop:
while (true) {
switch (currentToken) {
case TOK_PLUS:
next();
prop = evalAddition(prop.getNumeric(),
parseMultiplicativeExpr().getNumeric());
break;
case TOK_MINUS:
next();
prop = evalSubtraction(prop.getNumeric(),
parseMultiplicativeExpr().getNumeric());
break;
default:
break loop;
}
}
return prop;
}
/**
* Try to parse a multiply, divide or modulo expression and return
* the resulting Property.
*/
private Property parseMultiplicativeExpr() throws PropertyException {
Property prop = parseUnaryExpr();
loop:
while (true) {
switch (currentToken) {
case TOK_DIV:
next();
prop = evalDivide(prop.getNumeric(),
parseUnaryExpr().getNumeric());
break;
case TOK_MOD:
next();
prop = evalModulo(prop.getNumber(),
parseUnaryExpr().getNumber());
break;
case TOK_MULTIPLY:
next();
prop = evalMultiply(prop.getNumeric(),
parseUnaryExpr().getNumeric());
break;
default:
break loop;
}
}
return prop;
}
/**
* Try to parse a unary minus expression and return the
* resulting Property.
*/
private Property parseUnaryExpr() throws PropertyException {
if (currentToken == TOK_MINUS) {
next();
return evalNegate(parseUnaryExpr().getNumeric());
}
return parsePrimaryExpr();
}
/**
* Checks that the current token is a right parenthesis
* and throws an exception if this isn't the case.
*/
private void expectRpar() throws PropertyException {
if (currentToken != TOK_RPAR) {
throw new PropertyException("expected )");
}
next();
}
/**
* Try to parse a primary expression and return the
* resulting Property.
* A primary expression is either a parenthesized expression or an
* expression representing a primitive Property datatype, such as a
* string literal, an NCname, a number or a unit expression, or a
* function call expression.
*/
private Property parsePrimaryExpr() throws PropertyException {
Property prop;
if (currentToken == TOK_COMMA) {
//Simply skip commas, for example for font-family
next();
}
switch (currentToken) {
case TOK_LPAR:
next();
prop = parseAdditiveExpr();
expectRpar();
return prop;
case TOK_LITERAL:
prop = StringProperty.getInstance(currentTokenValue);
break;
case TOK_NCNAME:
// Interpret this in context of the property or do it later?
prop = new NCnameProperty(currentTokenValue);
break;
case TOK_FLOAT:
prop = NumberProperty.getInstance(new Double(currentTokenValue));
break;
case TOK_INTEGER:
prop = NumberProperty.getInstance(new Integer(currentTokenValue));
break;
case TOK_PERCENT:
/*
* Get the length base value object from the Maker. If null, then
* this property can't have % values. Treat it as a real number.
*/
double pcval = Double.parseDouble(
currentTokenValue.substring(0, currentTokenValue.length() - 1)) / 100.0;
PercentBase pcBase = this.propInfo.getPercentBase();
if (pcBase != null) {
if (pcBase.getDimension() == 0) {
prop = NumberProperty.getInstance(pcval * pcBase.getBaseValue());
} else if (pcBase.getDimension() == 1) {
if (pcBase instanceof LengthBase) {
if (pcval == 0.0) {
prop = FixedLength.ZERO_FIXED_LENGTH;
break;
}
//If the base of the percentage is known
//and absolute, it can be resolved by the
//parser
Length base = ((LengthBase)pcBase).getBaseLength();
if (base != null && base.isAbsolute()) {
prop = FixedLength.getInstance(pcval * base.getValue());
break;
}
}
prop = new PercentLength(pcval, pcBase);
} else {
throw new PropertyException("Illegal percent dimension value");
}
} else {
// WARNING? Interpret as a decimal fraction, eg. 50% = .5
prop = NumberProperty.getInstance(pcval);
}
break;
case TOK_NUMERIC:
// A number plus a valid unit name.
int numLen = currentTokenValue.length() - currentUnitLength;
String unitPart = currentTokenValue.substring(numLen);
double numPart = Double.parseDouble(currentTokenValue.substring(0, numLen));
if (RELUNIT.equals(unitPart)) {
prop = (Property) NumericOp.multiply(
NumberProperty.getInstance(numPart),
propInfo.currentFontSize());
} else {
if ("px".equals(unitPart)) {
//pass the ratio between target-resolution and
//the default resolution of 72dpi
float resolution = propInfo.getPropertyList().getFObj()
.getUserAgent().getSourceResolution();
prop = FixedLength.getInstance(
numPart, unitPart,
UnitConv.IN2PT / resolution);
} else {
//use default resolution of 72dpi
prop = FixedLength.getInstance(numPart, unitPart);
}
}
break;
case TOK_COLORSPEC:
prop = ColorProperty.getInstance(propInfo.getUserAgent(), currentTokenValue);
break;
case TOK_FUNCTION_LPAR:
Function function = (Function)FUNCTION_TABLE.get(currentTokenValue);
if (function == null) {
throw new PropertyException("no such function: "
+ currentTokenValue);
}
next();
// Push new function (for function context: getPercentBase())
propInfo.pushFunction(function);
if (function.nbArgs() < 0) {
// Negative nbArgs --> function with variable number of arguments
prop = function.eval(parseVarArgs(function), propInfo);
} else {
prop = function.eval(parseArgs(function), propInfo);
}
propInfo.popFunction();
return prop;
default:
// TODO: add the token or the expr to the error message.
throw new PropertyException("syntax error");
}
next();
return prop;
}
/**
* Parse a comma separated list of function arguments. Each argument
* may itself be an expression. This method consumes the closing right
* parenthesis of the argument list.
* @param function The function object for which the arguments are
* collected.
* @return An array of Property objects representing the arguments
* found.
* @throws PropertyException If the number of arguments found isn't equal
* to the number expected.
*/
Property[] parseArgs(Function function) throws PropertyException {
int nbArgs = function.nbArgs();
Property[] args = new Property[nbArgs];
Property prop;
int i = 0;
if (currentToken == TOK_RPAR) {
// No args: func()
next();
} else {
while (true) {
prop = parseAdditiveExpr();
if (i < nbArgs) {
args[i++] = prop;
}
// ignore extra args
if (currentToken != TOK_COMMA) {
break;
}
next();
}
expectRpar();
}
if (i == nbArgs - 1 && function.padArgsWithPropertyName()) {
args[i++] = StringProperty.getInstance(
propInfo.getPropertyMaker().getName());
}
if (nbArgs != i) {
throw new PropertyException("Expected " + nbArgs
+ ", but got " + i + " args for function");
}
return args;
}
/**
*
* Parse a comma separated list of function arguments. Each argument
* may itself be an expression. This method consumes the closing right
* parenthesis of the argument list.
*
* The method differs from parseArgs in that it accepts a variable
* number of arguments.
*
* @param function The function object for which the arguments are
* collected.
* @return An array of Property objects representing the arguments
* found.
* @throws PropertyException If the number of arguments found isn't equal
* to the number expected.
*
* TODO Merge this with parseArgs?
*/
Property[] parseVarArgs(Function function) throws PropertyException {
// For variable argument functions the minimum number of arguments is returned as a
// negative integer from the nbArgs method
int nbArgs = -function.nbArgs();
List args = new LinkedList();
Property prop;
if (currentToken == TOK_RPAR) {
// No args: func()
next();
} else {
while (true) {
prop = parseAdditiveExpr();
args.add(prop);
// ignore extra args
if (currentToken != TOK_COMMA) {
break;
}
next();
}
expectRpar();
}
if (nbArgs > args.size()) {
throw new PropertyException("Expected at least " + nbArgs
+ ", but got " + args.size() + " args for function");
}
Property[] propArray = new Property[args.size()];
args.toArray(propArray);
return propArray;
}
/**
* Evaluate an addition operation. If either of the arguments is null,
* this means that it wasn't convertible to a Numeric value.
* @param op1 A Numeric object (Number or Length-type object)
* @param op2 A Numeric object (Number or Length-type object)
* @return A new NumericProperty object holding an object which represents
* the sum of the two operands.
* @throws PropertyException If either operand is null.
*/
private Property evalAddition(Numeric op1,
Numeric op2) throws PropertyException {
if (op1 == null || op2 == null) {
throw new PropertyException("Non numeric operand in addition");
}
return (Property) NumericOp.addition(op1, op2);
}
/**
* Evaluate a subtraction operation. If either of the arguments is null,
* this means that it wasn't convertible to a Numeric value.
* @param op1 A Numeric object (Number or Length-type object)
* @param op2 A Numeric object (Number or Length-type object)
* @return A new NumericProperty object holding an object which represents
* the difference of the two operands.
* @throws PropertyException If either operand is null.
*/
private Property evalSubtraction(Numeric op1,
Numeric op2) throws PropertyException {
if (op1 == null || op2 == null) {
throw new PropertyException("Non numeric operand in subtraction");
}
return (Property) NumericOp.subtraction(op1, op2);
}
/**
* Evaluate a unary minus operation. If the argument is null,
* this means that it wasn't convertible to a Numeric value.
* @param op A Numeric object (Number or Length-type object)
* @return A new NumericProperty object holding an object which represents
* the negative of the operand (multiplication by *1).
* @throws PropertyException If the operand is null.
*/
private Property evalNegate(Numeric op) throws PropertyException {
if (op == null) {
throw new PropertyException("Non numeric operand to unary minus");
}
return (Property) NumericOp.negate(op);
}
/**
* Evaluate a multiplication operation. If either of the arguments is null,
* this means that it wasn't convertible to a Numeric value.
* @param op1 A Numeric object (Number or Length-type object)
* @param op2 A Numeric object (Number or Length-type object)
* @return A new NumericProperty object holding an object which represents
* the product of the two operands.
* @throws PropertyException If either operand is null.
*/
private Property evalMultiply(Numeric op1,
Numeric op2) throws PropertyException {
if (op1 == null || op2 == null) {
throw new PropertyException("Non numeric operand in multiplication");
}
return (Property) NumericOp.multiply(op1, op2);
}
/**
* Evaluate a division operation. If either of the arguments is null,
* this means that it wasn't convertible to a Numeric value.
* @param op1 A Numeric object (Number or Length-type object)
* @param op2 A Numeric object (Number or Length-type object)
* @return A new NumericProperty object holding an object which represents
* op1 divided by op2.
* @throws PropertyException If either operand is null.
*/
private Property evalDivide(Numeric op1,
Numeric op2) throws PropertyException {
if (op1 == null || op2 == null) {
throw new PropertyException("Non numeric operand in division");
}
return (Property) NumericOp.divide(op1, op2);
}
/**
* Evaluate a modulo operation. If either of the arguments is null,
* this means that it wasn't convertible to a Number value.
* @param op1 A Number object
* @param op2 A Number object
* @return A new NumberProperty object holding an object which represents
* op1 mod op2.
* @throws PropertyException If either operand is null.
*/
private Property evalModulo(Number op1,
Number op2) throws PropertyException {
if (op1 == null || op2 == null) {
throw new PropertyException("Non number operand to modulo");
}
return NumberProperty.getInstance(op1.doubleValue() % op2.doubleValue());
}
}