001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: /* $Id: PropertyParser.java 554251 2007-07-07 20:13:41Z adelmelle $ */
019:
020: package org.apache.fop.fo.expr;
021:
022: import org.apache.fop.apps.FOUserAgent;
023: import org.apache.fop.datatypes.Numeric;
024: import org.apache.fop.datatypes.PercentBase;
025: import org.apache.fop.fo.properties.ColorProperty;
026: import org.apache.fop.fo.properties.FixedLength;
027: import org.apache.fop.fo.properties.ListProperty;
028: import org.apache.fop.fo.properties.NumberProperty;
029: import org.apache.fop.fo.properties.PercentLength;
030: import org.apache.fop.fo.properties.Property;
031: import org.apache.fop.fo.properties.StringProperty;
032:
033: import java.util.HashMap;
034: import java.util.LinkedList;
035: import java.util.List;
036:
037: /**
038: * Class to parse XSL-FO property expressions.
039: * This class is heavily based on the epxression parser in James Clark's
040: * XT, an XSLT processor.
041: */
042: public final class PropertyParser extends PropertyTokenizer {
043: private PropertyInfo propInfo; // Maker and propertyList related info
044:
045: private static final String RELUNIT = "em";
046: private static final HashMap FUNCTION_TABLE = new HashMap();
047:
048: static {
049: // Initialize the HashMap of XSL-defined functions
050: FUNCTION_TABLE.put("ceiling", new CeilingFunction());
051: FUNCTION_TABLE.put("floor", new FloorFunction());
052: FUNCTION_TABLE.put("round", new RoundFunction());
053: FUNCTION_TABLE.put("min", new MinFunction());
054: FUNCTION_TABLE.put("max", new MaxFunction());
055: FUNCTION_TABLE.put("abs", new AbsFunction());
056: FUNCTION_TABLE.put("rgb", new RGBColorFunction());
057: FUNCTION_TABLE.put("system-color", new SystemColorFunction());
058: FUNCTION_TABLE.put("from-table-column",
059: new FromTableColumnFunction());
060: FUNCTION_TABLE.put("inherited-property-value",
061: new InheritedPropFunction());
062: FUNCTION_TABLE.put("from-parent", new FromParentFunction());
063: FUNCTION_TABLE.put("from-nearest-specified-value",
064: new NearestSpecPropFunction());
065: FUNCTION_TABLE.put("proportional-column-width",
066: new PPColWidthFunction());
067: FUNCTION_TABLE.put("label-end", new LabelEndFunction());
068: FUNCTION_TABLE.put("body-start", new BodyStartFunction());
069: FUNCTION_TABLE.put("rgb-icc", new ICCColorFunction());
070: FUNCTION_TABLE.put("cmyk", new CMYKcolorFunction()); //non-standard!!!
071:
072: /**
073: * * NOT YET IMPLEMENTED!!!
074: * FUNCTION_TABLE.put("system-font", new SystemFontFunction());
075: * FUNCTION_TABLE.put("merge-property-values", new MergePropsFunction());
076: */
077: }
078:
079: /**
080: * Public entrypoint to the Property expression parser.
081: * @param expr The specified value (attribute on the xml element).
082: * @param propInfo A PropertyInfo object representing the context in
083: * which the property expression is to be evaluated.
084: * @return A Property object holding the parsed result.
085: * @throws PropertyException If the "expr" cannot be parsed as a Property.
086: */
087: public static Property parse(String expr, PropertyInfo propInfo)
088: throws PropertyException {
089: try {
090: return new PropertyParser(expr, propInfo).parseProperty();
091: } catch (PropertyException exc) {
092: exc.setPropertyInfo(propInfo);
093: throw exc;
094: }
095: }
096:
097: /**
098: * Private constructor. Called by the static parse() method.
099: * @param propExpr The specified value (attribute on the xml element).
100: * @param propInfo A PropertyInfo object representing the context in
101: * which the property expression is to be evaluated.
102: */
103: private PropertyParser(String propExpr, PropertyInfo pInfo) {
104: super (propExpr);
105: this .propInfo = pInfo;
106: }
107:
108: /**
109: * Parse the property expression described in the instance variables.
110: * Note: If the property expression String is empty, a StringProperty
111: * object holding an empty String is returned.
112: * @return A Property object holding the parsed result.
113: * @throws PropertyException If the "expr" cannot be parsed as a Property.
114: */
115: private Property parseProperty() throws PropertyException {
116: next();
117: if (currentToken == TOK_EOF) {
118: // if prop value is empty string, force to StringProperty
119: return StringProperty.getInstance("");
120: }
121: ListProperty propList = null;
122: while (true) {
123: Property prop = parseAdditiveExpr();
124: if (currentToken == TOK_EOF) {
125: if (propList != null) {
126: propList.addProperty(prop);
127: return propList;
128: } else {
129: return prop;
130: }
131: } else {
132: if (propList == null) {
133: propList = new ListProperty(prop);
134: } else {
135: propList.addProperty(prop);
136: }
137: }
138: // throw new PropertyException("unexpected token");
139: }
140: // return prop;
141: }
142:
143: /**
144: * Try to parse an addition or subtraction expression and return the
145: * resulting Property.
146: */
147: private Property parseAdditiveExpr() throws PropertyException {
148: // Evaluate and put result on the operand stack
149: Property prop = parseMultiplicativeExpr();
150: loop: while (true) {
151: switch (currentToken) {
152: case TOK_PLUS:
153: next();
154: prop = evalAddition(prop.getNumeric(),
155: parseMultiplicativeExpr().getNumeric());
156: break;
157: case TOK_MINUS:
158: next();
159: prop = evalSubtraction(prop.getNumeric(),
160: parseMultiplicativeExpr().getNumeric());
161: break;
162: default:
163: break loop;
164: }
165: }
166: return prop;
167: }
168:
169: /**
170: * Try to parse a multiply, divide or modulo expression and return
171: * the resulting Property.
172: */
173: private Property parseMultiplicativeExpr() throws PropertyException {
174: Property prop = parseUnaryExpr();
175: loop: while (true) {
176: switch (currentToken) {
177: case TOK_DIV:
178: next();
179: prop = evalDivide(prop.getNumeric(), parseUnaryExpr()
180: .getNumeric());
181: break;
182: case TOK_MOD:
183: next();
184: prop = evalModulo(prop.getNumber(), parseUnaryExpr()
185: .getNumber());
186: break;
187: case TOK_MULTIPLY:
188: next();
189: prop = evalMultiply(prop.getNumeric(), parseUnaryExpr()
190: .getNumeric());
191: break;
192: default:
193: break loop;
194: }
195: }
196: return prop;
197: }
198:
199: /**
200: * Try to parse a unary minus expression and return the
201: * resulting Property.
202: */
203: private Property parseUnaryExpr() throws PropertyException {
204: if (currentToken == TOK_MINUS) {
205: next();
206: return evalNegate(parseUnaryExpr().getNumeric());
207: }
208: return parsePrimaryExpr();
209: }
210:
211: /**
212: * Checks that the current token is a right parenthesis
213: * and throws an exception if this isn't the case.
214: */
215: private void expectRpar() throws PropertyException {
216: if (currentToken != TOK_RPAR) {
217: throw new PropertyException("expected )");
218: }
219: next();
220: }
221:
222: /**
223: * Try to parse a primary expression and return the
224: * resulting Property.
225: * A primary expression is either a parenthesized expression or an
226: * expression representing a primitive Property datatype, such as a
227: * string literal, an NCname, a number or a unit expression, or a
228: * function call expression.
229: */
230: private Property parsePrimaryExpr() throws PropertyException {
231: Property prop;
232: if (currentToken == TOK_COMMA) {
233: //Simply skip commas, for example for font-family
234: next();
235: }
236: switch (currentToken) {
237: case TOK_LPAR:
238: next();
239: prop = parseAdditiveExpr();
240: expectRpar();
241: return prop;
242:
243: case TOK_LITERAL:
244: prop = StringProperty.getInstance(currentTokenValue);
245: break;
246:
247: case TOK_NCNAME:
248: // Interpret this in context of the property or do it later?
249: prop = new NCnameProperty(currentTokenValue);
250: break;
251:
252: case TOK_FLOAT:
253: prop = NumberProperty.getInstance(new Double(
254: currentTokenValue));
255: break;
256:
257: case TOK_INTEGER:
258: prop = NumberProperty.getInstance(new Integer(
259: currentTokenValue));
260: break;
261:
262: case TOK_PERCENT:
263: /*
264: * Get the length base value object from the Maker. If null, then
265: * this property can't have % values. Treat it as a real number.
266: */
267: double pcval = new Double(currentTokenValue.substring(0,
268: currentTokenValue.length() - 1)).doubleValue() / 100.0;
269: PercentBase pcBase = this .propInfo.getPercentBase();
270: if (pcBase != null) {
271: if (pcBase.getDimension() == 0) {
272: prop = NumberProperty.getInstance(pcval
273: * pcBase.getBaseValue());
274: } else if (pcBase.getDimension() == 1) {
275: prop = new PercentLength(pcval, pcBase);
276: } else {
277: throw new PropertyException(
278: "Illegal percent dimension value");
279: }
280: } else {
281: // WARNING? Interpret as a decimal fraction, eg. 50% = .5
282: prop = NumberProperty.getInstance(pcval);
283: }
284: break;
285:
286: case TOK_NUMERIC:
287: // A number plus a valid unit name.
288: int numLen = currentTokenValue.length() - currentUnitLength;
289: String unitPart = currentTokenValue.substring(numLen);
290: Double numPart = new Double(currentTokenValue.substring(0,
291: numLen));
292: if (unitPart.equals(RELUNIT)) {
293: prop = (Property) NumericOp.multiply(NumberProperty
294: .getInstance(numPart.doubleValue()), propInfo
295: .currentFontSize());
296: } else {
297: prop = FixedLength.getInstance(numPart.doubleValue(),
298: unitPart);
299: }
300: break;
301:
302: case TOK_COLORSPEC:
303: FOUserAgent ua = (propInfo == null) ? null : (propInfo
304: .getFO() == null ? null : propInfo.getFO()
305: .getUserAgent());
306: prop = new ColorProperty(ua, currentTokenValue);
307: break;
308:
309: case TOK_FUNCTION_LPAR:
310: Function function = (Function) FUNCTION_TABLE
311: .get(currentTokenValue);
312: if (function == null) {
313: throw new PropertyException("no such function: "
314: + currentTokenValue);
315: }
316: next();
317: // Push new function (for function context: getPercentBase())
318: propInfo.pushFunction(function);
319: if (function.nbArgs() < 0) {
320: // Negative nbArgs --> function with variable number of arguments
321: prop = function.eval(parseVarArgs(function), propInfo);
322: } else {
323: prop = function.eval(parseArgs(function), propInfo);
324: }
325: propInfo.popFunction();
326: return prop;
327:
328: default:
329: // TODO: add the token or the expr to the error message.
330: throw new PropertyException("syntax error");
331: }
332: next();
333: return prop;
334: }
335:
336: /**
337: * Parse a comma separated list of function arguments. Each argument
338: * may itself be an expression. This method consumes the closing right
339: * parenthesis of the argument list.
340: * @param function The function object for which the arguments are
341: * collected.
342: * @return An array of Property objects representing the arguments
343: * found.
344: * @throws PropertyException If the number of arguments found isn't equal
345: * to the number expected.
346: */
347: Property[] parseArgs(Function function) throws PropertyException {
348: int nbArgs = function.nbArgs();
349: Property[] args = new Property[nbArgs];
350: Property prop;
351: int i = 0;
352: if (currentToken == TOK_RPAR) {
353: // No args: func()
354: next();
355: } else {
356: while (true) {
357:
358: prop = parseAdditiveExpr();
359: if (i < nbArgs) {
360: args[i++] = prop;
361: }
362: // ignore extra args
363: if (currentToken != TOK_COMMA) {
364: break;
365: }
366: next();
367: }
368: expectRpar();
369: }
370: if (i == nbArgs - 1 && function.padArgsWithPropertyName()) {
371: args[i++] = StringProperty.getInstance(propInfo
372: .getPropertyMaker().getName());
373: }
374: if (nbArgs != i) {
375: throw new PropertyException("Expected " + nbArgs
376: + ", but got " + i + " args for function");
377: }
378: return args;
379: }
380:
381: /**
382: *
383: * Parse a comma separated list of function arguments. Each argument
384: * may itself be an expression. This method consumes the closing right
385: * parenthesis of the argument list.
386: *
387: * The method differs from parseArgs in that it accepts a variable
388: * number of arguments.
389: *
390: * @param function The function object for which the arguments are
391: * collected.
392: * @return An array of Property objects representing the arguments
393: * found.
394: * @throws PropertyException If the number of arguments found isn't equal
395: * to the number expected.
396: *
397: * TODO Merge this with parseArgs?
398: */
399: Property[] parseVarArgs(Function function) throws PropertyException {
400: // For variable argument functions the minimum number of arguments is returned as a
401: // negative integer from the nbArgs method
402: int nbArgs = -function.nbArgs();
403: List args = new LinkedList();
404: Property prop;
405: if (currentToken == TOK_RPAR) {
406: // No args: func()
407: next();
408: } else {
409: while (true) {
410: prop = parseAdditiveExpr();
411: args.add(prop);
412: // ignore extra args
413: if (currentToken != TOK_COMMA) {
414: break;
415: }
416: next();
417: }
418: expectRpar();
419: }
420: if (nbArgs > args.size()) {
421: throw new PropertyException("Expected at least " + nbArgs
422: + ", but got " + args.size() + " args for function");
423: }
424: Property[] propArray = new Property[args.size()];
425: args.toArray(propArray);
426: return propArray;
427: }
428:
429: /**
430: * Evaluate an addition operation. If either of the arguments is null,
431: * this means that it wasn't convertible to a Numeric value.
432: * @param op1 A Numeric object (Number or Length-type object)
433: * @param op2 A Numeric object (Number or Length-type object)
434: * @return A new NumericProperty object holding an object which represents
435: * the sum of the two operands.
436: * @throws PropertyException If either operand is null.
437: */
438: private Property evalAddition(Numeric op1, Numeric op2)
439: throws PropertyException {
440: if (op1 == null || op2 == null) {
441: throw new PropertyException(
442: "Non numeric operand in addition");
443: }
444: return (Property) NumericOp.addition(op1, op2);
445: }
446:
447: /**
448: * Evaluate a subtraction operation. If either of the arguments is null,
449: * this means that it wasn't convertible to a Numeric value.
450: * @param op1 A Numeric object (Number or Length-type object)
451: * @param op2 A Numeric object (Number or Length-type object)
452: * @return A new NumericProperty object holding an object which represents
453: * the difference of the two operands.
454: * @throws PropertyException If either operand is null.
455: */
456: private Property evalSubtraction(Numeric op1, Numeric op2)
457: throws PropertyException {
458: if (op1 == null || op2 == null) {
459: throw new PropertyException(
460: "Non numeric operand in subtraction");
461: }
462: return (Property) NumericOp.subtraction(op1, op2);
463: }
464:
465: /**
466: * Evaluate a unary minus operation. If the argument is null,
467: * this means that it wasn't convertible to a Numeric value.
468: * @param op A Numeric object (Number or Length-type object)
469: * @return A new NumericProperty object holding an object which represents
470: * the negative of the operand (multiplication by *1).
471: * @throws PropertyException If the operand is null.
472: */
473: private Property evalNegate(Numeric op) throws PropertyException {
474: if (op == null) {
475: throw new PropertyException(
476: "Non numeric operand to unary minus");
477: }
478: return (Property) NumericOp.negate(op);
479: }
480:
481: /**
482: * Evaluate a multiplication operation. If either of the arguments is null,
483: * this means that it wasn't convertible to a Numeric value.
484: * @param op1 A Numeric object (Number or Length-type object)
485: * @param op2 A Numeric object (Number or Length-type object)
486: * @return A new NumericProperty object holding an object which represents
487: * the product of the two operands.
488: * @throws PropertyException If either operand is null.
489: */
490: private Property evalMultiply(Numeric op1, Numeric op2)
491: throws PropertyException {
492: if (op1 == null || op2 == null) {
493: throw new PropertyException(
494: "Non numeric operand in multiplication");
495: }
496: return (Property) NumericOp.multiply(op1, op2);
497: }
498:
499: /**
500: * Evaluate a division operation. If either of the arguments is null,
501: * this means that it wasn't convertible to a Numeric value.
502: * @param op1 A Numeric object (Number or Length-type object)
503: * @param op2 A Numeric object (Number or Length-type object)
504: * @return A new NumericProperty object holding an object which represents
505: * op1 divided by op2.
506: * @throws PropertyException If either operand is null.
507: */
508: private Property evalDivide(Numeric op1, Numeric op2)
509: throws PropertyException {
510: if (op1 == null || op2 == null) {
511: throw new PropertyException(
512: "Non numeric operand in division");
513: }
514: return (Property) NumericOp.divide(op1, op2);
515: }
516:
517: /**
518: * Evaluate a modulo operation. If either of the arguments is null,
519: * this means that it wasn't convertible to a Number value.
520: * @param op1 A Number object
521: * @param op2 A Number object
522: * @return A new NumberProperty object holding an object which represents
523: * op1 mod op2.
524: * @throws PropertyException If either operand is null.
525: */
526: private Property evalModulo(Number op1, Number op2)
527: throws PropertyException {
528: if (op1 == null || op2 == null) {
529: throw new PropertyException("Non number operand to modulo");
530: }
531: return NumberProperty.getInstance(op1.doubleValue()
532: % op2.doubleValue());
533: }
534:
535: }
|