/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* 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
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.perseus.parser;
import java.util.Date;
import java.util.Vector;
import com.sun.perseus.model.TimedElementNode;
import com.sun.perseus.model.TimedElementSupport;
import com.sun.perseus.model.AccessKeyCondition;
import com.sun.perseus.model.EventBaseCondition;
import com.sun.perseus.model.OffsetCondition;
import com.sun.perseus.model.RepeatCondition;
import com.sun.perseus.model.SyncBaseCondition;
import com.sun.perseus.model.TimeCondition;
import com.sun.perseus.util.SVGConstants;
/**
* Parser for SVG time condition values, as defined in the SVG specification
* for timing attributes:
* http://www.w3.org/TR/SVG11/animate.html#TimingAttributes
*
* @author <a href="mailto:christopher.campbell@sun.com">Chris Campbell</a>
* @version $Id: TimeConditionParser.java,v 1.4 2006/04/21 06:40:40 st125089 Exp $
*/
public class TimeConditionParser extends ClockParser {
/**
* The list of time conditions being parsed.
*/
private Vector conditions;
/**
* If true, a "begin" attribute is being parsed; otherwise, an "end"
* attribute is being parsed.
*/
private boolean isBegin;
/**
* The TimedElementNode for the attribute being parsed.
*/
private TimedElementNode ten;
/**
* The TimedElementSupport for the TimedElementNode associated with
* the attribute being parsed.
*/
private TimedElementSupport tes;
/**
* Parses a begin/end timing attribute value. This method throws an
* <code>IllegalArgumentException</code> if the input argument's
* syntax does not conform to that of a clock value, as defined
* by the SVG animate tag specification.
*
* @param attrValue the value to parse.
* @param ten the TimedElementNode for which the attribute is parsed.
* @param isBegin defines whether this is a begin attribute.
* @return an array of <code>TimeConditions</code> corresponding to the
* input attrubute value.
*/
public TimeCondition[] parseBeginEndAttribute(final String attrValue,
final TimedElementNode ten,
final boolean isBegin) {
setString(attrValue);
if (attrValue.length() == 0) {
throw new IllegalArgumentException();
}
this.conditions = new Vector();
this.ten = ten;
this.tes = ten.getTimedElementSupport();
this.isBegin = isBegin;
loop: for (;;) {
// parse leading whitespace
current = read();
skipSpaces();
m1: switch (current) {
case ';':
throw new IllegalArgumentException();
case -1:
if (isBegin && (conditions.size() == 0)) {
// an empty begin attribute maps to offset(0)
conditions.addElement(new OffsetCondition(tes, isBegin, 0L));
}
break loop;
case '+': case '-':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
parseOffset();
break m1;
default:
if (currentStartsWith("accessKey(")) {
parseAccessKey();
} else if (currentStartsWith("repeat(")) {
parseRepeat(null);
} else if (currentStartsWith("indefinite")) {
// indefinite maps to no time condition
break m1;
} else {
parseOther();
}
break m1;
}
// parse trailing whitespace (current should point to the character
// just following the last time condition)
skipSpaces();
// detect ';' separator, or -1
m2: switch (current) {
case ';':
break m2;
case -1:
break loop;
default:
throw new IllegalArgumentException();
}
}
TimeCondition[] ret = new TimeCondition[conditions.size()];
conditions.copyInto(ret);
return ret;
}
/**
* Helper method that parses a signed clock value (one that may or may
* not be preceded by a '-' or '+' sign. Returns the parsed clock value
* as a long offset. If the clock string begins with a '-' sign, the
* returned offset value will be negative. If the clock string begins
* with a '+' sign (or is unsigned), the returned offset value will be
* positive.
*
* @return a signed long offset value (in milliseconds)
* @see ClockParser
*/
protected final long parseSignedClockValue() {
boolean pos = true;
switch (current) {
case '-':
pos = false;
// FALLTHROUGH
case '+':
current = read();
skipSpaces();
break;
default:
break;
}
long offset = parseClock(false);
if (!pos) {
offset = -offset;
}
return offset;
}
/**
* Parses an optional offset value. If the current character is a sign
* value ('+' or '-'), the offset value is parsed and returned as a long
* value. If the offset value is not present (<code>current</code> is
* ';' or -1), this method returns zero. If some other character is
* encountered, an <code>IllegalArgumentException</code> is thrown.
*
* @return a signed long offset value (in milliseconds)
* @see #parseSignedClockValue
*/
protected final long parseOptionalOffset() {
long offset = 0L;
switch (current) {
case '+': case '-':
offset = parseSignedClockValue();
break;
case ';':
case -1:
break;
default:
throw new IllegalArgumentException("Unsupported character in optional offset value: " + ((char) current));
}
return offset;
}
/**
* Parses an offset time condition. The current character is assumed
* to be '+', '-', or a numeral. If parsing is successful, an
* OffsetCondition is added to the list of time conditions and the
* current character is the character that immediately follows the offset
* value.
*
* @see #parseSignedClockValue
*/
protected final void parseOffset() {
long offset = parseSignedClockValue();
conditions.addElement(new OffsetCondition(tes, isBegin, offset));
}
/**
* Parses an accessKey time condition. The current character is assumed
* to be the first character after "accesskey(". If parsing is successful,
* an AccessKeyCondition is added to the list of time conditions and the
* current character is the character that immediately follows the
* access key value (either the ')' or the optional offset).
*/
protected final void parseAccessKey() {
// parse access key character (followed by ')')
if ((current == -1) || (current == ')')) {
throw new IllegalArgumentException();
}
char accesskey = (char) current;
current = read();
if (current != ')') {
throw new IllegalArgumentException();
}
// parse whitespace followed by optional offset
current = read();
skipSpaces();
long offset = parseOptionalOffset();
conditions.addElement(new AccessKeyCondition(tes, isBegin,
offset, accesskey));
}
/**
* Parses a repeat time condition. The current character is assumed
* to be the first character after "repeat(". If parsing is successful,
* a RepeatCondition is added to the list of time conditions and the
* current character is the character that immediately follows the
* repeat value (either the ')' or the optional offset).
*
* @param id the identifier for this time condition; if null, the event
* base id will be "unspecified"
*/
protected final void parseRepeat(final String id) {
// parse iteration value (followed by ')')
int repeatCount = 0;
loop: for (;;) {
switch (current) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
repeatCount = repeatCount * 10 + (current - '0');
break;
case ')':
break loop;
default:
throw new IllegalArgumentException("Illegal character in repeat condition: " + ((char) current));
}
current = read();
}
// parse whitespace followed by optional offset
current = read();
skipSpaces();
long offset = parseOptionalOffset();
conditions.addElement(new RepeatCondition(tes, isBegin, id,
offset, repeatCount));
}
/**
* Parses a syncbase time condition. The current character is assumed
* to be the first character after "begin" or "end". If parsing is
* successful, a SyncBaseCondition is added to the list of time conditions
* and the current character is the character that immediately follows the
* syncbase value (either the "begin", "end", or the optional offset).
*
* @param id the identifier for this time condition; should not be null
* @param isBeginSync true if this condition is on the syncBase's begin
* condition; false if this condition is on the syncBase's end condition.
*/
protected final void parseSyncBase(final String id,
final boolean isBeginSync) {
// parse whitespace followed by optional offset
skipSpaces();
long offset = parseOptionalOffset();
conditions.addElement(new SyncBaseCondition(ten, isBegin, id,
isBeginSync, offset));
}
/**
* Parses an event base time condition. If <code>id</code> is
* <code>null</code>, the event base id will be "unspecified", and the
* eventType is assumed to be non-<code>null</code> (the current character
* is assumed to be the first character after the event type). If
* <code>eventType</code> is <code>null</code>, the event type will be
* parsed (the current character is assumed to be the first character after
* the event base id and dot, e.g. "foo.").
*
* If parsing is successful, an EventBaseCondition is added to the list
* of time conditions and the current character is the character that
* immediately follows the event base value (either the id, eventType,
* or the optional offset).
*
* @param id the identifier for this time condition; can be null
* @param eventType the event type for this time condition; can be null
* if the id was already parsed
*/
protected final void parseEvent(final String id, String eventType) {
if (eventType == null) {
StringBuffer etbuf = new StringBuffer();
loop: for (;;) {
switch (current) {
case '\\':
// skip escape characters
current = read();
switch (current) {
case '.':
case '-':
case '_':
etbuf.append((char) current);
break;
default:
if (isLetterOrDigit((char) current)) {
etbuf.append((char) current);
} else {
throw new IllegalArgumentException();
}
break;
}
break;
case '.':
throw new IllegalArgumentException();
case '+':
case '-':
case ';':
case -1:
if (etbuf.length() == 0) {
throw new IllegalArgumentException();
}
eventType = etbuf.toString();
break loop;
case 0x20:
case 0x09:
case 0x0D:
case 0x0A:
skipSpaces();
switch (current) {
case '+':
case '-':
case ';':
case -1:
// no id, must be an event
if (etbuf.length() == 0) {
throw new IllegalArgumentException();
}
eventType = etbuf.toString();
break loop;
default:
throw new IllegalArgumentException();
}
default:
etbuf.append((char) current);
break;
}
current = read();
}
}
// parse optional offset (whitespace has already been parsed)
long offset = parseOptionalOffset();
conditions.addElement(new EventBaseCondition(tes, isBegin,
id, toDOMEventType(eventType),
offset));
}
/**
* Parses a time condition with an optional id prefix. Delegates to one
* of the other <code>parse*()</code> methods, depending on whether the
* string matches a repeat, syncbase, or event time condition.
*/
protected final void parseOther() {
StringBuffer idbuf = new StringBuffer();
String id = null;
String eventType = null;
loopid: for (;;) {
switch (current) {
case '\\':
// skip escape characters
current = read();
switch (current) {
case '.':
case '-':
case '_':
idbuf.append((char) current);
break;
default:
if (isLetterOrDigit((char) current)) {
idbuf.append((char) current);
} else {
throw new IllegalArgumentException();
}
break;
}
break;
case '.':
if (idbuf.length() == 0) {
throw new IllegalArgumentException();
}
id = idbuf.toString();
current = read();
break loopid;
case '+':
case '-':
case ';':
case -1:
// no id, must be an event (we will never enter this method
// with one of these characters as the current character, so
// idbuf will always have a non-zero length at this point)
eventType = idbuf.toString();
break loopid;
case 0x20:
case 0x09:
case 0x0D:
case 0x0A:
skipSpaces();
switch (current) {
case '+':
case '-':
case ';':
case -1:
// no id, must be an event (we will never enter this method
// with one of these characters as the current character,
// so idbuf will always have a non-zero length at this
// point)
eventType = idbuf.toString();
break loopid;
default:
throw new IllegalArgumentException();
}
default:
idbuf.append((char) current);
break;
}
current = read();
}
if (id != null) {
if (currentStartsWith("repeat(")) {
parseRepeat(id);
} else if (currentStartsWith("begin")) {
parseSyncBase(id, true);
} else if (currentStartsWith("end")) {
parseSyncBase(id, false);
} else {
parseEvent(id, null);
}
} else {
parseEvent(null, eventType);
}
}
/**
* Do not use Character.isLetterOrDigit because it is not supported on
* all Java platforms.
*
* @param c the character to test.
* @return true if c is a letter or a digit.
*/
public static final boolean isLetterOrDigit(final char c) {
return (Character.isDigit(c) || Character.isUpperCase(c) || Character.isLowerCase(c));
}
/**
* Converts the event to the DOMEvent type. This is required because for some
* events, the name used in the SMIL begin/end attribute is not the same as
* the DOM event so some translation is needed.
*
* @param smilEventType the name of the animation event type.
* @return the DOM Level 2 Event name.
*
* @see http://www.w3.org/TR/SVG11/interact.html#SVGEvents
*/
public static final String toDOMEventType(final String smilEventType) {
if (SVGConstants.SVG_SMIL_FOCUS_IN_EVENT_TYPE.equals(smilEventType)) {
return SVGConstants.SVG_DOMFOCUSIN_EVENT_TYPE;
} else if (SVGConstants.SVG_SMIL_FOCUS_OUT_EVENT_TYPE.equals(smilEventType)) {
return SVGConstants.SVG_DOMFOCUSOUT_EVENT_TYPE;
} else if (SVGConstants.SVG_SMIL_ACTIVATE_EVENT_TYPE.equals(smilEventType)) {
return SVGConstants.SVG_DOMACTIVATE_EVENT_TYPE;
}
return smilEventType;
}
}
|