/*!
* 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 java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
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.dom.DocumentContext;
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;
/**
* 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);
}
}