/******************************************************************************* * * Copyright 2010 Alexandru Craciun, and individual contributors as indicated * by the @authors tag. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This software 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. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. ******************************************************************************/ package org.netxilia.api.impl.display; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import org.netxilia.api.INetxiliaSystem; import org.netxilia.api.command.CellCommands; import org.netxilia.api.display.IStyleFormatter; import org.netxilia.api.display.Style; import org.netxilia.api.display.StyleAttribute; import org.netxilia.api.display.StyleDefinition; import org.netxilia.api.display.StyleGroup; import org.netxilia.api.event.CellEvent; import org.netxilia.api.event.ColumnEvent; import org.netxilia.api.event.ISheetEventListener; import org.netxilia.api.event.RowEvent; import org.netxilia.api.event.SheetEvent; import org.netxilia.api.exception.AlreadyExistsException; import org.netxilia.api.exception.NetxiliaBusinessException; import org.netxilia.api.exception.NetxiliaResourceException; import org.netxilia.api.exception.NotFoundException; import org.netxilia.api.exception.StorageException; import org.netxilia.api.impl.model.SheetNames; import org.netxilia.api.impl.model.WorkbookIds; import org.netxilia.api.model.CellData; import org.netxilia.api.model.ISheet; import org.netxilia.api.model.SheetType; import org.netxilia.api.model.WorkbookId; import org.netxilia.api.reference.AreaReference; import org.netxilia.api.user.AclPrivilegedMode; import org.netxilia.api.utils.Matrix; import org.netxilia.api.value.IGenericValue; import org.netxilia.api.value.StringValue; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; /** * This repository returns the style definition that are written in a specific spreadsheet in each workbook. The SYSTEM * workbook contains the default definitions for the formatters and styles present in the interface. * * Each workbook can overwrite the definition of a style. * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ public class StyleRepositoryImpl implements IStyleRepository, ApplicationContextAware { private static final int COL_DESCRIPTION = 4; private static final int COL_NAME = 3; private static final int COL_ATTS = 2; private static final int COL_ID = 1; private static final int COL_GROUP = 0; private static final int ROW_SIZE = 5; private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(StyleRepositoryImpl.class); @Autowired private INetxiliaSystem workbookProcessor; private String stylesSheetName = SheetNames.STYLES; private String formatterPrefix = "formatter-"; private final Ehcache cache; private ApplicationContext context; private ISheetEventListener definitionInvalidator = new InvalidateDefinitionCache(); public StyleRepositoryImpl() { cache = CacheManager.create().getEhcache("style-cache"); } public String getStylesSheetName() { return stylesSheetName; } public void setStylesSheetName(String stylesSheetName) { this.stylesSheetName = stylesSheetName; } public String getFormatterPrefix() { return formatterPrefix; } public void setFormatterPrefix(String formatterPrefix) { this.formatterPrefix = formatterPrefix; } private String safeString(List<CellData> row, int col) { if (col >= row.size()) { return null; } CellData cell = row.get(col); return cell != null && cell.getValue() != null ? cell.getValue().getStringValue().trim() : null; } /** * Columns * * <pre> * A=group * B=style id * C=name (what to put in a menu for example) * D=description * F=attributes (; separated) * </pre> * * @throws NetxiliaBusinessException * @throws NetxiliaResourceException * */ private WorkbookStyleDefinitions loadDefinitions(WorkbookId workbookId) throws NetxiliaResourceException, NetxiliaBusinessException { List<StyleDefinition> styles = new ArrayList<StyleDefinition>(); ISheet sheet = null; try { sheet = workbookProcessor.getWorkbook(workbookId).getSheet(stylesSheetName); Matrix<CellData> cells = sheet.receiveCells(AreaReference.ALL).getNonBlocking(); StyleGroup lastGroup = null; for (List<CellData> row : cells.getRows()) { if (row.size() < 2) { continue; } String group = safeString(row, COL_GROUP); String id = safeString(row, COL_ID); String atts = safeString(row, COL_ATTS); String name = safeString(row, COL_NAME); String desc = safeString(row, COL_DESCRIPTION); StyleGroup styleGroup = group != null && group.length() > 0 ? new StyleGroup(group) : lastGroup; lastGroup = styleGroup; if (id != null && id.length() > 0 && atts != null && atts.length() > 0) { List<StyleAttribute> styleAttributes = parseAttributes(atts); name = name != null ? name : id; StyleDefinition def = new StyleDefinition(new Style(id), styleGroup, name, desc, styleAttributes); getFormatter(findAttribute(styleAttributes, StyleAttribute.PATTERN_TYPE), def); styles.add(def); } } sheet.addListener(definitionInvalidator); } catch (NotFoundException e) { // no problem - use the parent one } if (workbookId.equals(WorkbookIds.SYSTEM)) { return new WorkbookStyleDefinitions(this, null, styles); } return new WorkbookStyleDefinitions(this, WorkbookIds.SYSTEM, styles); } private IStyleFormatter getFormatter(String type, StyleDefinition definition) { if (type == null) { return null; } IStyleFormatter formatter = (IStyleFormatter) context.getBean(formatterPrefix + type, definition); if (formatter == null) { log.warn("Unknown formatter:" + type); } else { definition.setFormatter(formatter); } return formatter; } private String findAttribute(List<StyleAttribute> attributes, String name) { for (StyleAttribute att : attributes) { if (name.equals(att.getName())) { return att.getValue(); } } return null; } private List<StyleAttribute> parseAttributes(String atts) { List<StyleAttribute> attributes = new ArrayList<StyleAttribute>(); int index = 0; while (index < atts.length()) { int nameEnd = atts.indexOf(':', index); if (nameEnd < 0) { break; } int valueEnd = indexOf(atts, ';', nameEnd + 1); if (valueEnd == -1) { valueEnd = atts.length(); } attributes.add(new StyleAttribute(atts.substring(index, nameEnd).trim(), removeQuotes(atts.substring( nameEnd + 1, valueEnd).trim()))); index = valueEnd + 1; } return attributes; } private String removeQuotes(String str) { if (str.length() < 2) { return str; } if (str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'') { return str.substring(1, str.length() - 1); } // what to do with unbalanced quotes !? return str; } /** * looks for the first position of the given character, from the given position, by skipping characters inside * simple quotes * * @param atts * @param c * @param i * @return */ private int indexOf(String s, char c, int index) { boolean inQuotes = false; for (int i = index; i < s.length(); ++i) { char crtChar = s.charAt(i); if (crtChar == c) { if (!inQuotes) { return i; } } else if (crtChar == '\'') { inQuotes = !inQuotes; } } return -1; } private String attributesToString(Collection<StyleAttribute> attributes) { if (attributes == null || attributes.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); for (StyleAttribute att : attributes) { // TODO quote values when necessary sb.append(att.getName()).append(":").append(att.getValue()).append(";"); } return sb.toString(); } @Override public WorkbookStyleDefinitions getDefinitions(WorkbookId workbookId) throws NetxiliaResourceException, NetxiliaBusinessException { Element element; if ((element = cache.get(workbookId)) != null) { return (WorkbookStyleDefinitions) element.getObjectValue(); } boolean wasSet = AclPrivilegedMode.set(); try { WorkbookStyleDefinitions defs = loadDefinitions(workbookId); cache.put(new Element(workbookId, defs)); return defs; } finally { if (!wasSet) { AclPrivilegedMode.clear(); } } } @Override public void setApplicationContext(ApplicationContext context) throws BeansException { this.context = context; } @Override public void addDefinition(WorkbookId workbookId, StyleDefinition definition) throws StorageException, NotFoundException { Assert.notNull(definition); ISheet formatterSheet = getFormatterSheet(workbookId); IGenericValue[] row = new IGenericValue[5]; row[COL_ID] = new StringValue(definition.getId().getId()); row[COL_NAME] = new StringValue(definition.getName()); row[COL_DESCRIPTION] = new StringValue(definition.getDescription()); row[COL_GROUP] = new StringValue(definition.getGroup().getId()); row[COL_ATTS] = new StringValue(attributesToString(definition.getAttributes().values())); formatterSheet.sendCommand(CellCommands.rowValues(AreaReference.lastRow(0, ROW_SIZE - 1), Arrays.asList(row))); cache.remove(workbookId); } protected ISheet getFormatterSheet(WorkbookId workbookId) throws StorageException, NotFoundException { try { return workbookProcessor.getWorkbook(workbookId).getSheet(stylesSheetName); } catch (NotFoundException e) { ISheet formatterSheet = null; try { formatterSheet = workbookProcessor.getWorkbook(workbookId).addNewSheet(stylesSheetName, SheetType.normal); } catch (AlreadyExistsException e1) { // nothing to do } return formatterSheet; } } private class InvalidateDefinitionCache implements ISheetEventListener { @Override public void onSheetEvent(SheetEvent sheetEvent) { cache.remove(sheetEvent.getSheetName().getWorkbookId()); } @Override public void onRowEvent(RowEvent rowEvent) { cache.remove(rowEvent.getSheetName().getWorkbookId()); } @Override public void onColumnEvent(ColumnEvent columnEvent) { cache.remove(columnEvent.getSheetName().getWorkbookId()); } @Override public void onCellEvent(CellEvent cellEvent) { cache.remove(cellEvent.getSheetName().getWorkbookId()); // log.info("Remove from cache:" + cellEvent.getSheetName().getWorkbookId() + ": size:" + cache.getSize() // + "," + cache.getKeys()); } } }