/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 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.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.libraries.css.parser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.base.util.FastStack;
import org.pentaho.reporting.libraries.css.PageAreaType;
import org.pentaho.reporting.libraries.css.model.CSSDeclarationRule;
import org.pentaho.reporting.libraries.css.model.CSSFontFaceRule;
import org.pentaho.reporting.libraries.css.model.CSSMediaRule;
import org.pentaho.reporting.libraries.css.model.CSSPageAreaRule;
import org.pentaho.reporting.libraries.css.model.CSSPageRule;
import org.pentaho.reporting.libraries.css.model.CSSStyleRule;
import org.pentaho.reporting.libraries.css.model.StyleKeyRegistry;
import org.pentaho.reporting.libraries.css.model.StyleRule;
import org.pentaho.reporting.libraries.css.model.StyleSheet;
import org.pentaho.reporting.libraries.css.selectors.CSSSelector;
import org.pentaho.reporting.libraries.resourceloader.DependencyCollector;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.DocumentHandler;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.SACMediaList;
import org.w3c.css.sac.Selector;
import org.w3c.css.sac.SelectorList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Creation-Date: 23.11.2005, 13:06:06
*
* @author Thomas Morgner
*/
public class StyleSheetHandler implements DocumentHandler, ErrorHandler {
private Log logger = LogFactory.getLog( StyleSheetHandler.class );
private HashMap namespaces;
private StyleSheet styleSheet;
private FastStack parentRules;
private CSSDeclarationRule styleRule;
private StyleKeyRegistry styleKeyRegistry;
private ResourceManager resourceManager;
private ResourceKey source;
private DependencyCollector dependencies;
private String defaultNamespace;
public StyleSheetHandler() {
this.namespaces = new HashMap();
this.parentRules = new FastStack();
}
public void init( final StyleKeyRegistry styleKeyRegistry,
final ResourceManager resourceManager,
final ResourceKey source,
final long version,
final StyleRule parentRule ) {
if ( styleKeyRegistry == null ) {
throw new NullPointerException();
}
if ( resourceManager == null ) {
throw new NullPointerException();
}
this.styleKeyRegistry = styleKeyRegistry;
this.resourceManager = resourceManager;
this.source = source;
if ( source != null ) {
this.dependencies = new DependencyCollector( source, version );
}
this.parentRules.clear();
if ( parentRule != null ) {
parentRules.push( parentRule );
}
this.namespaces.clear();
}
// public void init(final ResourceManager manager,
// final ResourceKey source,
// final long version,
// final StyleKeyRegistry registry,
// final StyleRule parentRule)
// {
// if (registry == null)
// {
// throw new NullPointerException();
// }
//
// this.registry = registry;
//
// this.parentRules.clear();
// if (parentRule != null)
// {
// parentRules.push(parentRule);
// }
//
// this.manager = manager;
// this.source = source;
// if (source != null)
// {
// this.dependencies = new DependencyCollector(source, version);
// }
//
// this.namespaces.clear();
// }
public void registerNamespace( String prefix, String uri ) {
if ( prefix == null ) {
throw new NullPointerException();
}
if ( uri == null ) {
throw new NullPointerException();
}
namespaces.put( prefix, uri );
}
public String getDefaultNamespaceURI() {
return defaultNamespace;
}
public void setDefaultNamespaceURI( final String defaultNamespace ) {
this.defaultNamespace = defaultNamespace;
}
public ResourceKey getSource() {
return source;
}
public DependencyCollector getDependencies() {
return dependencies;
}
public CSSDeclarationRule getStyleRule() {
return styleRule;
}
public void setStyleRule( final CSSDeclarationRule styleRule ) {
this.styleRule = styleRule;
}
public StyleSheet getStyleSheet() {
return styleSheet;
}
public void setStyleSheet( final StyleSheet styleSheet ) {
this.styleSheet = styleSheet;
}
public ResourceManager getResourceManager() {
return resourceManager;
}
public void initParseContext( InputSource source ) {
// the default namespace might be fed from outside ..
final CSSParserContext parserContext = CSSParserContext.getContext();
parserContext.setNamespaces( namespaces );
parserContext.setStyleKeyRegistry( styleKeyRegistry );
parserContext.setSource( getSource() );
}
/**
* Receive notification of the beginning of a style sheet.
* <p/>
* The CSS parser will invoke this method only once, before any other methods in this interface.
*
* @param source the input source
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void startDocument( InputSource source )
throws CSSException {
initParseContext( source );
if ( this.styleSheet == null ) {
this.styleSheet = new StyleSheet();
this.styleSheet.setSource( getSource() );
}
}
/**
* Receive notification of the end of a document.
* <p/>
* The CSS parser will invoke this method only once, and it will be the last method invoked during the parse. The
* parser shall not invoke this method until it has either abandoned parsing (because of an unrecoverable error) or
* reached the end of input.
*
* @param source the input source
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void endDocument( InputSource source )
throws CSSException {
final Iterator entries = namespaces.entrySet().iterator();
while ( entries.hasNext() ) {
final Map.Entry entry = (Map.Entry) entries.next();
final String prefix = (String) entry.getKey();
final String uri = (String) entry.getValue();
styleSheet.addNamespace( prefix, uri );
}
}
/**
* Receive notification of a comment. If the comment appears in a declaration (e.g. color: /* comment * / blue;), the
* parser notifies the comment before the declaration.
*
* @param text The comment.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void comment( String text )
throws CSSException {
// comments are ignored ..
}
/**
* Receive notification of an unknown rule t-rule not supported by this parser.
*
* @param atRule The complete ignored at-rule.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void ignorableAtRule( String atRule )
throws CSSException {
StringTokenizer strtok = new StringTokenizer( atRule );
if ( strtok.hasMoreTokens() == false ) {
return;
}
String ruleName = strtok.nextToken();
if ( ruleName.equalsIgnoreCase( "@namespace" ) ) {
parseNamespaceRule( strtok );
} else if ( styleRule instanceof CSSPageRule ) {
CSSPageRule pageRule = (CSSPageRule) styleRule;
if ( ruleName.length() <= 1 ) {
return;
}
String areaName = ruleName.substring( 1 );
final PageAreaType[] pageAreas = PageAreaType.getPageAreas();
for ( int i = 0; i < pageAreas.length; i++ ) {
PageAreaType pageArea = pageAreas[ i ];
if ( areaName.equalsIgnoreCase( pageArea.getName() ) ) {
final CSSPageAreaRule areaRule = parsePageRule( pageArea, atRule );
if ( areaRule != null ) {
pageRule.addRule( areaRule );
}
return;
}
}
logger.info( "Did not recognize page @rule: " + atRule );
} else {
logger.info( "Ignorable @rule: " + atRule );
}
}
private CSSPageAreaRule parsePageRule( PageAreaType areaType, String atRule ) {
final ResourceKey source = this.source;
final CSSPageAreaRule areaRule =
new CSSPageAreaRule( styleSheet, styleRule, areaType );
final int firstBrace = atRule.indexOf( '{' );
final int lastBrace = atRule.indexOf( '}' );
if ( firstBrace < 0 || lastBrace < firstBrace ) {
// cannot parse that ..
return null;
}
StyleSheetParserUtil.getInstance().parseStyleRule
( namespaces, atRule.substring( firstBrace + 1, lastBrace - 1 ),
source, areaRule, getResourceManager(), styleKeyRegistry );
return areaRule;
}
private void parseNamespaceRule( final StringTokenizer strtok ) {
String next = strtok.nextToken();
final String prefix;
final String uri;
if ( next.startsWith( "url(" ) ) {
prefix = "";
uri = next;
} else {
prefix = next;
if ( strtok.hasMoreTokens() == false ) {
return;
}
uri = strtok.nextToken();
}
int uriStart = uri.indexOf( '(' );
if ( uriStart == -1 ) {
return;
}
int uriEnd = uri.indexOf( ')' );
if ( uriEnd == -1 ) {
return;
}
if ( uriStart > uriEnd ) {
return;
}
final String uriValue = uri.substring( uriStart + 1, uriEnd );
namespaceDeclaration( prefix, uriValue );
}
/**
* Receive notification of an unknown rule t-rule not supported by this parser.
*
* @param prefix <code>null</code> if this is the default namespace
* @param uri The URI for this namespace.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void namespaceDeclaration( String prefix, String uri )
throws CSSException {
if ( prefix == null || "".equals( prefix ) ) {
this.namespaces.put( "", uri );
this.defaultNamespace = uri;
CSSParserContext.getContext().setDefaultNamespace( defaultNamespace );
} else {
this.namespaces.put( prefix, uri );
}
}
/**
* Receive notification of a import statement in the style sheet.
*
* @param uri The URI of the imported style sheet.
* @param media The intended destination media for style information.
* @param defaultNamespaceURI The default namespace URI for the imported style sheet.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void importStyle( String uri,
SACMediaList media,
String defaultNamespaceURI )
throws CSSException {
// instantiate a new parser and parse the stylesheet.
final ResourceManager manager = getResourceManager();
if ( manager == null ) {
// there is no source set, so we have no resource manager, and thus
// we do no parsing.
//
// This should only be the case if we parse style-values; in that case
// include-statement are not supported anyway.
return;
}
try {
CSSParserContext.getContext().setDefaultNamespace( defaultNamespaceURI );
final ResourceKey key;
if ( source == null ) {
key = manager.createKey( uri );
} else {
key = manager.deriveKey( source, uri );
}
final Resource res = manager.create( key, source, StyleSheet.class );
if ( res == null ) {
return;
}
final StyleSheet styleSheet = (StyleSheet) res.getResource();
this.styleSheet.addStyleSheet( styleSheet );
} catch ( ResourceException e ) {
// ignore ..
} finally {
CSSParserContext.getContext().setStyleKeyRegistry( styleKeyRegistry );
CSSParserContext.getContext().setSource( getSource() );
CSSParserContext.getContext().setNamespaces( namespaces );
CSSParserContext.getContext().setDefaultNamespace( defaultNamespace );
}
}
/**
* Receive notification of the beginning of a media statement.
* <p/>
* The Parser will invoke this method at the beginning of every media statement in the style sheet. there will be a
* corresponding endMedia() event for every startElement() event.
*
* @param media The intended destination media for style information.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void startMedia( SACMediaList media )
throws CSSException {
// ignore for now ..
styleRule = new CSSMediaRule( styleSheet, getParentRule() );
parentRules.push( styleRule );
}
/**
* Receive notification of the end of a media statement.
*
* @param media The intended destination media for style information.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void endMedia( SACMediaList media )
throws CSSException {
parentRules.pop();
styleSheet.addRule( styleRule );
styleRule = null;
}
/**
* Receive notification of the beginning of a page statement.
* <p/>
* The Parser will invoke this method at the beginning of every page statement in the style sheet. there will be a
* corresponding endPage() event for every startPage() event.
*
* @param name the name of the page (if any, null otherwise)
* @param pseudo_page the pseudo page (if any, null otherwise)
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void startPage( String name, String pseudo_page )
throws CSSException {
// Log.debug ("Page Rule: " + name + " / " + pseudo_page);
// yes, we have to parse that.
styleRule = new CSSPageRule( styleSheet, getParentRule(), name, pseudo_page );
parentRules.push( styleRule );
}
/**
* Receive notification of the end of a media statement.
*
* @param name The intended destination medium for style information.
* @param pseudo_page the pseudo page (if any, null otherwise)
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void endPage( String name, String pseudo_page )
throws CSSException {
parentRules.pop();
styleSheet.addRule( styleRule );
styleRule = null;
}
/**
* Receive notification of the beginning of a font face statement.
* <p/>
* The Parser will invoke this method at the beginning of every font face statement in the style sheet. there will be
* a corresponding endFontFace() event for every startFontFace() event.
*
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void startFontFace()
throws CSSException {
// font-face events are ignored for now.
styleRule = new CSSFontFaceRule( styleSheet, getParentRule() );
parentRules.push( styleRule );
}
protected StyleRule getParentRule() {
if ( parentRules.isEmpty() == false ) {
return (StyleRule) parentRules.peek();
}
return null;
}
/**
* Receive notification of the end of a font face statement.
*
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void endFontFace()
throws CSSException {
parentRules.pop();
}
/**
* Receive notification of the beginning of a rule statement.
*
* @param selectors All intended selectors for all declarations.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void startSelector( SelectorList selectors )
throws CSSException {
styleRule = new CSSStyleRule( styleSheet, getParentRule() );
}
/**
* Receive notification of the end of a rule statement.
*
* @param selectors All intended selectors for all declarations.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void endSelector( SelectorList selectors )
throws CSSException {
if ( styleRule.isEmpty() ) {
return;
}
int length = selectors.getLength();
for ( int i = 0; i < length; i++ ) {
final Selector selector = selectors.item( i );
try {
final CSSStyleRule rule = (CSSStyleRule) styleRule.clone();
rule.setSelector( (CSSSelector) selector );
styleSheet.addRule( rule );
} catch ( CloneNotSupportedException e ) {
// should not happen
}
}
}
/**
* Receive notification of a declaration.
*
* @param name the name of the property.
* @param value the value of the property. All whitespace are stripped.
* @param important is this property important ?
* @throws CSSException Any CSS exception, possibly wrapping another exception.
*/
public void property( String name, LexicalUnit value, boolean important )
throws CSSException {
CSSValueFactory factory = CSSParserContext.getContext().getValueFactory();
try {
factory.parseValue( styleRule, name, value, important );
} catch ( Exception e ) {
// we catch everything.
logger.warn( "Error parsing style key: " + name, e );
}
}
/**
* Receive notification of a warning. <p/> <p>CSS parsers will use this method to report conditions that are not
* errors or fatal errors as defined by the XML 1.0 recommendation. The default behaviour is to take no action.</p>
* <p/> <p>The CSS parser must continue to provide normal parsing events after invoking this method: it should still
* be possible for the application to process the document through to the end.</p>
*
* @param exception The warning information encapsulated in a CSS parse exception.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
* @see CSSParseException
*/
public void warning( CSSParseException exception )
throws CSSException {
logger.warn( "Warning: " + exception.getMessage() );
}
/**
* Receive notification of a recoverable error. <p/> <p>This corresponds to the definition of "error" in section 1.2
* of the W3C XML 1.0 Recommendation. For example, a validating parser would use this callback to report the
* violation of a validity constraint. The default behaviour is to take no action.</p> <p/> <p>The CSS parser must
* continue to provide normal parsing events after invoking this method: it should still be possible for the
* application to process the document through to the end. If the application cannot do so, then the parser should
* report a fatal error even if the XML 1.0 recommendation does not require it to do so.</p>
*
* @param exception The error information encapsulated in a CSS parse exception.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
* @see CSSParseException
*/
public void error( CSSParseException exception )
throws CSSException {
logger.warn( "Error: ", exception );
}
/**
* Receive notification of a non-recoverable error. <p/> <p>This corresponds to the definition of "fatal error" in
* section 1.2 of the W3C XML 1.0 Recommendation. For example, a parser would use this callback to report the
* violation of a well-formedness constraint.</p> <p/> <p>The application must assume that the document is unusable
* after the parser has invoked this method, and should continue (if at all) only for the sake of collecting addition
* error messages: in fact, CSS parsers are free to stop reporting any other events once this method has been
* invoked.</p>
*
* @param exception The error information encapsulated in a CSS parse exception.
* @throws CSSException Any CSS exception, possibly wrapping another exception.
* @see CSSParseException
*/
public void fatalError( CSSParseException exception )
throws CSSException {
logger.warn( "Fatal Error: ", exception );
}
}