package org.jboss.seam.excel.css; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.faces.component.UIComponent; import org.apache.commons.beanutils.PropertyUtils; import org.jboss.seam.core.Interpolator; import org.jboss.seam.excel.ExcelWorkbookException; import org.jboss.seam.excel.ui.UILink; import org.jboss.seam.log.Log; import org.jboss.seam.log.Logging; /** * CSS parser for the XLS-CSS * * @author Nicklas Karlsson (nickarls@gmail.com) */ public class CSSParser { // Where to look for the style private static final String STYLE_ATTRIBUTE = "style"; // Where to look for the style class private static final String STYLE_CLASS_ATTRIBUTE = "styleClass"; // What separates multiple style class references private static final String STYLE_SHORTHAND_SEPARATOR = " "; // What starts a rule block in a CSS file private static final String LEFT_BRACE = "{"; // What ends a rule block in a CSS file private static final String RIGHT_BRACE = "}"; // The style classes that have been read in from e:link referenced, mapped on // style class name private Map<String, StyleMap> definedStyleClasses = new HashMap<String, StyleMap>(); // The registered property builders, mapped on attribute name private Map<String, PropertyBuilder> propertyBuilders = new HashMap<String, PropertyBuilder>(); // A cache of previously parsed css, mapped on component private Map<UIComponent, StyleMap> cellStyleCache = new HashMap<UIComponent, StyleMap>(); private Log log = Logging.getLog(CSSParser.class); /** * Constructor, initializes the property builders */ public CSSParser() { initPropertyBuilders(); } /** * Constructor with stylesheets * * @param stylesheets The list of referenced stylesheets in UILink elements * @throws MalformedURLException If the URL was bad * @throws IOException If the URL could not be read */ public CSSParser(List<UILink> stylesheets) throws MalformedURLException, IOException { initPropertyBuilders(); loadStylesheets(stylesheets); } /** * Loads stylesheets (merging by class name) * * @param stylesheets The stylesheets to read/merge * @throws MalformedURLException If the URL was bad * @throws IOException If the URL could not be read */ private void loadStylesheets(List<UILink> stylesheets) throws MalformedURLException, IOException { for (UILink stylesheet : stylesheets) { definedStyleClasses.putAll(parseStylesheet(stylesheet.getURL())); } } /** * Registers the property builders */ private void initPropertyBuilders() { propertyBuilders.put(CSSNames.FONT_FAMILY, new PropertyBuilders.FontFamily()); propertyBuilders.put(CSSNames.FONT_SIZE, new PropertyBuilders.FontSize()); propertyBuilders.put(CSSNames.FONT_COLOR, new PropertyBuilders.FontColor()); propertyBuilders.put(CSSNames.FONT_ITALIC, new PropertyBuilders.FontItalic()); propertyBuilders.put(CSSNames.FONT_SCRIPT_STYLE, new PropertyBuilders.FontScriptStyle()); propertyBuilders.put(CSSNames.FONT_STRUCK_OUT, new PropertyBuilders.FontStruckOut()); propertyBuilders.put(CSSNames.FONT_UNDERLINE_STYLE, new PropertyBuilders.FontUnderlineStyle()); propertyBuilders.put(CSSNames.FONT_BOLD, new PropertyBuilders.FontBold()); propertyBuilders.put(CSSNames.FONT, new PropertyBuilders.FontShorthand()); propertyBuilders.put(CSSNames.BACKGROUND_PATTERN, new PropertyBuilders.BackgroundPattern()); propertyBuilders.put(CSSNames.BACKGROUND_COLOR, new PropertyBuilders.BackgroundColor()); propertyBuilders.put(CSSNames.BACKGROUND, new PropertyBuilders.BackgroundShorthand()); propertyBuilders.put(CSSNames.BORDER_LEFT_COLOR, new PropertyBuilders.BorderLeftColor()); propertyBuilders.put(CSSNames.BORDER_LEFT_LINE_STYLE, new PropertyBuilders.BorderLeftLineStyle()); propertyBuilders.put(CSSNames.BORDER_LEFT, new PropertyBuilders.BorderLeftShorthand()); propertyBuilders.put(CSSNames.BORDER_TOP_COLOR, new PropertyBuilders.BorderTopColor()); propertyBuilders.put(CSSNames.BORDER_TOP_LINE_STYLE, new PropertyBuilders.BorderTopLineStyle()); propertyBuilders.put(CSSNames.BORDER_TOP, new PropertyBuilders.BorderTopShorthand()); propertyBuilders.put(CSSNames.BORDER_RIGHT_COLOR, new PropertyBuilders.BorderRightColor()); propertyBuilders.put(CSSNames.BORDER_RIGHT_LINE_STYLE, new PropertyBuilders.BorderRightLineStyle()); propertyBuilders.put(CSSNames.BORDER_RIGHT, new PropertyBuilders.BorderRightShorthand()); propertyBuilders.put(CSSNames.BORDER_BOTTOM_COLOR, new PropertyBuilders.BorderBottomColor()); propertyBuilders.put(CSSNames.BORDER_BOTTOM_LINE_STYLE, new PropertyBuilders.BorderBottomLineStyle()); propertyBuilders.put(CSSNames.BORDER_BOTTOM, new PropertyBuilders.BorderBottomShorthand()); propertyBuilders.put(CSSNames.BORDER, new PropertyBuilders.BorderShorthand()); propertyBuilders.put(CSSNames.FORMAT_MASK, new PropertyBuilders.FormatMask()); propertyBuilders.put(CSSNames.ALIGNMENT, new PropertyBuilders.Alignment()); propertyBuilders.put(CSSNames.INDENTATION, new PropertyBuilders.Indentation()); propertyBuilders.put(CSSNames.ORIENTATION, new PropertyBuilders.Orientation()); propertyBuilders.put(CSSNames.LOCKED, new PropertyBuilders.Locked()); propertyBuilders.put(CSSNames.SHRINK_TO_FIT, new PropertyBuilders.ShrinkToFit()); propertyBuilders.put(CSSNames.WRAP, new PropertyBuilders.Wrap()); propertyBuilders.put(CSSNames.VERICAL_ALIGNMENT, new PropertyBuilders.VericalAlignment()); propertyBuilders.put(CSSNames.COLUMN_WIDTH, new PropertyBuilders.ColumnWidth()); propertyBuilders.put(CSSNames.COLUMN_AUTO_SIZE, new PropertyBuilders.ColumnAutoSize()); propertyBuilders.put(CSSNames.COLUMN_HIDDEN, new PropertyBuilders.ColumnHidden()); propertyBuilders.put(CSSNames.COLUMN_EXPORT, new PropertyBuilders.ColumnExport()); propertyBuilders.put(CSSNames.COLUMN_WIDTHS, new PropertyBuilders.ColumnWidths()); propertyBuilders.put(CSSNames.FORCE_TYPE, new PropertyBuilders.ForceType()); } /** * Reads data from an URL to a String * * @param url The URL to read * @return The read data as a String * @throws IOException If the stream could not be read */ private static String readCSS(InputStream in) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuffer buffer = new StringBuffer(); String line; while ((line = reader.readLine()) != null) { buffer.append(line); } reader.close(); return buffer.toString(); } /** * Parses a style sheet. Really crude. Assumes data is nicely formatted on * one line per entry * * @param urlString The URL to read * @return A map of style class names mapped to StyleMaps * @throws MalformedURLException * @throws IOException */ private Map<String, StyleMap> parseStylesheet(String urlString) throws MalformedURLException, IOException { Map<String, StyleMap> styleClasses = new HashMap<String, StyleMap>(); InputStream cssStream = null; if (urlString.indexOf("://") < 0) { cssStream = getClass().getResourceAsStream(urlString); } else { cssStream = new URL(urlString).openStream(); } String css = readCSS(cssStream).toLowerCase(); int firstBrace = -1; int secondBrace = -1; while (!"".equals(css)) { firstBrace = css.indexOf(LEFT_BRACE); if (firstBrace >= 0) { secondBrace = css.indexOf(RIGHT_BRACE, firstBrace + 1); } if (firstBrace >= 0 && secondBrace >= 0 && firstBrace != secondBrace) { String styleName = css.substring(0, firstBrace).trim(); if (styleName.startsWith(".")) { styleName = styleName.substring(1); } String styleString = css.substring(firstBrace + 1, secondBrace).trim(); StyleMap styleMap = StyleStringParser.of(styleString, propertyBuilders).parse(); styleClasses.put(styleName, styleMap); css = css.substring(secondBrace + 1); } else { css = ""; } } return styleClasses; } /** * Gets style from a component * * @param component The component to examine * @return null if not found, otherwise style string */ public static String getStyle(UIComponent component) { return getStyleProperty(component, STYLE_ATTRIBUTE); } /** * Gets style class from a component * * @param component The component to examine * @return null if not found, otherwise style class(es) string */ public static String getStyleClass(UIComponent component) { return getStyleProperty(component, STYLE_CLASS_ATTRIBUTE); } /** * Reads a property from a component * * @param component The component to examine * @param field The field to read * @return The value from the field */ private static String getStyleProperty(UIComponent component, String field) { try { return (String) PropertyUtils.getProperty(component, field); } catch (NoSuchMethodException e) { // No panic, no property return null; } catch (Exception e) { String message = Interpolator.instance().interpolate("Could not read field #0 of bean #1", field, component.getId()); throw new ExcelWorkbookException(message, e); } } /** * Cascades on parents, collecting them into list * * @param component The component to examine * @param styleMaps The list of collected style maps * @return The list of style maps */ private List<StyleMap> cascadeStyleMap(UIComponent component, List<StyleMap> styleMaps) { styleMaps.add(getStyleMap(component)); if (component.getParent() != null) { cascadeStyleMap(component.getParent(), styleMaps); } return styleMaps; } /** * Gets the cascaded style map for a component. Recurses on parents, * collecting style maps. The reverses the list and merges the styles * * @param component The component to examine * @return The merged style map */ public StyleMap getCascadedStyleMap(UIComponent component) { List<StyleMap> styleMaps = cascadeStyleMap(component, new ArrayList<StyleMap>()); Collections.reverse(styleMaps); StyleMap cascadedStyleMap = new StyleMap(); for (StyleMap styleMap : styleMaps) { cascadedStyleMap.putAll(styleMap); } return cascadedStyleMap; } /** * Gets a style map for a component (from cache if available) * * @param component The component to examine * @return The style map of the component */ private StyleMap getStyleMap(UIComponent component) { if (cellStyleCache.containsKey(component)) { return cellStyleCache.get(component); } StyleMap styleMap = new StyleMap(); String componentStyleClass = getStyleProperty(component, STYLE_CLASS_ATTRIBUTE); if (componentStyleClass != null) { String[] styleClasses = StyleStringParser.trimArray(componentStyleClass.split(STYLE_SHORTHAND_SEPARATOR)); for (String styleClass : styleClasses) { if (!definedStyleClasses.containsKey(styleClass)) { log.warn("Uknown style class #0", styleClass); continue; } styleMap.putAll(definedStyleClasses.get(styleClass)); } } String componentStyle = getStyleProperty(component, STYLE_ATTRIBUTE); if (componentStyle != null) { styleMap.putAll(StyleStringParser.of(componentStyle, propertyBuilders).parse()); } cellStyleCache.put(component, styleMap); return styleMap; } // private StyleMap parseStyleString(String styleString) // { // StyleMap styleMap = new StyleMap(); // // String[] styles = trimArray(styleString.split(STYLES_SEPARATOR)); // for (String style : styles) // { // int breakpoint = style.indexOf(STYLE_NAME_VALUE_SEPARATOR); // if (breakpoint < 0) { // log.warn("Style component #0 should be of form <key>#1<value>", style, STYLE_NAME_VALUE_SEPARATOR); // continue; // } // String styleName = style.substring(0, breakpoint).toLowerCase().trim(); // if (!propertyBuilders.containsKey(styleName)) // { // log.warn("No property builder (unknown style) for property #0", styleName); // continue; // } // PropertyBuilder propertyBuilder = propertyBuilders.get(styleName); // String styleValue = style.substring(breakpoint + 1); // String[] styleValues = trimArray(styleValue.trim().split(STYLE_SHORTHAND_SEPARATOR)); // styleMap.putAll(propertyBuilder.parseProperty(styleName, styleValues)); // } // // return styleMap; // } /** * Setter for stylesheets. Loads them also. * * @param stylesheets The stylesheets to load * @throws MalformedURLException If the URL is bad * @throws IOException If the URL cannot be read */ public void setStylesheets(List<UILink> stylesheets) throws MalformedURLException, IOException { loadStylesheets(stylesheets); } }