package com.idega.content.themes.helpers.business; import java.io.InputStream; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import bsh.Interpreter; import com.idega.content.business.ContentConstants; import com.idega.content.themes.helpers.bean.Theme; import com.idega.content.themes.helpers.bean.ThemeStyleGroupMember; import com.idega.core.file.util.MimeTypeUtil; import com.idega.core.search.business.SearchResult; import com.idega.slide.business.IWSlideService; import com.idega.util.CoreConstants; import com.idega.util.IOUtil; import com.idega.util.ListUtil; import com.idega.util.StringHandler; /** * @author <a href="mailto:valdas@idega.com">Valdas Žemaitis</a> * @version $Revision: 1.5 $ * * Calculates color value (hex value) from given expression * * Last modified: $Date: 2009/06/12 10:52:36 $ by $Author: valdas $ */ @Service @Scope(BeanDefinition.SCOPE_SINGLETON) public class ColourExpressionCalculator { private static final Logger logger = Logger.getLogger(ColourExpressionCalculator.class.getName()); private static final String[] _SEARCH_TERMS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"}; private static final List<String> SEARCH_TERMS = Collections.unmodifiableList(Arrays.asList(_SEARCH_TERMS)); private static final String[] _COLOUR_MATH_OPERATIONS = {CoreConstants.PLUS, CoreConstants.MINUS, CoreConstants.STAR}; private static final List<String> COLOUR_MATH_OPERATIONS = Collections.unmodifiableList(Arrays.asList(_COLOUR_MATH_OPERATIONS)); private static final String DEFAULT_COLOUR = "#ffffff"; private static final String RED_COLOUR_START = "r("; private static final String GREEN_COLOUR_START = "g("; private static final String BLUE_COLOUR_START = "b("; private static final Map<String, Integer> HEX_TO_DEC_NUMBERS_CACHE = new HashMap<String, Integer>(); @Autowired private ThemesHelper helper; @Autowired private ThemeChanger themeChanger; private Interpreter mathInterpreter = null; public ColourExpressionCalculator() { mathInterpreter = new bsh.Interpreter(); } private List<String> getOriginalColourFilesBySearch(Theme theme) { String searchScope = theme.getLinkToBase(); if (!searchScope.startsWith(CoreConstants.WEBDAV_SERVLET_URI)) { searchScope = new StringBuffer(CoreConstants.WEBDAV_SERVLET_URI).append(searchScope).toString(); } List<SearchResult> searchResults = getHelper().search("*_original.css", searchScope); if (searchResults == null || searchResults.isEmpty()) { return null; } List<String> files = new ArrayList<String>(); for (SearchResult result: searchResults) { files.add(StringHandler.remove(result.getSearchResultURI(), searchScope)); } return files; } protected boolean setValuesToColourFiles(Theme theme) { if (theme == null) { return false; } if (!theme.hasColourFiles()) { return true; } List<String> originalColourFiles = theme.getOriginalColourFiles(); boolean addOriginalFile = false; if (ListUtil.isEmpty(originalColourFiles)) { originalColourFiles = getOriginalColourFilesBySearch(theme); addOriginalFile = true; } if (originalColourFiles == null || originalColourFiles.isEmpty()) { logger.log(Level.WARNING, "No colour files found for theme: " + theme.getName() + ", " + theme.getLinkToBase()); return false; } IWSlideService slide = getHelper().getSlideService(); if (slide == null) { return false; } List<String> keys = theme.getStyleVariablesKeys(); String file = null; String originalColourFile = null; String webRoot = getHelper().getFullWebRoot(); String sourceLink = null; InputStream stream = null; String key = null; for (int i = 0; i < originalColourFiles.size(); i++) { originalColourFile = originalColourFiles.get(i); file = getColourFileByOriginalFile(theme, originalColourFile); if (file == null) { logger.log(Level.WARNING, "Colour file for original CSS file ('" + originalColourFile + "') was not found, can't set new values!"); continue; } sourceLink = new StringBuffer(webRoot).append(theme.getLinkToBase()).append(originalColourFile).toString(); stream = getHelper().getInputStream(sourceLink); if (stream == null) { logger.log(Level.WARNING, "Can't read CSS file from: " + sourceLink); continue; } String content = null; try { content = StringHandler.getContentFromInputStream(stream); } catch (Exception e) { logger.log(Level.SEVERE, "Error while reading content from stream: " + sourceLink, e); } finally { IOUtil.closeInputStream(stream); } if (content == null) { logger.log(Level.WARNING, "No content in CSS file: " + sourceLink); continue; } for (int j = 0; (j < keys.size() && content != null); j++) { key = keys.get(j); content = replaceColourFileContent(content, key, theme.getStyleVariableValue(key)); } content = checkIfAllVariablesReplaced(content, theme); if (content == null) { logger.log(Level.WARNING, "No content in CSS file: " + sourceLink); continue; } try { if (!(slide.uploadFileAndCreateFoldersFromStringAsRoot(theme.getLinkToBase(), file, content, MimeTypeUtil.MIME_TYPE_CSS, true))) { logger.log(Level.WARNING, "Error while writing file: " + file); continue; } } catch (RemoteException e) { logger.log(Level.SEVERE, "Error while writing file: " + file, e); } if (addOriginalFile) { theme.addOriginalColourFile(originalColourFile); } } return true; } private String getColourFileByOriginalFile(Theme theme, String originalFile) { List<String> files = theme.getColourFiles(); if (files == null || files.isEmpty()) { return null; } String fileStart = null; int underIndex = originalFile.indexOf(CoreConstants.UNDER); if (underIndex == -1) { return null; } fileStart = originalFile.substring(0, underIndex); for (String file: files) { if (file.startsWith(fileStart)) { return file; } } return null; } private String checkIfAllVariablesReplaced(String content, Theme theme) { if (content == null || theme == null) { return null; } int start = -1; int end = -1; List<String> searchTerm = ListUtil.convertStringArrayToList(new String[] {CoreConstants.PERCENT}); String cssExpression = null; String fixedCssExpression = null; String variable = null; String styleValue = null; ThemeStyleGroupMember colourVariation = null; while (content.indexOf(CoreConstants.PERCENT) != -1) { cssExpression = null; fixedCssExpression = null; variable = null; styleValue = null; colourVariation = null; start = getStartIndexForCssVariable(content, CoreConstants.PERCENT, CoreConstants.PERCENT); end = getEndIndexForCssVariable(content, CoreConstants.PERCENT, searchTerm, true, true); if (!(canSubstring(content, start, end))) { return content; } cssExpression = content.substring(start, end); variable = StringHandler.remove(cssExpression, CoreConstants.PERCENT); styleValue = theme.getStyleVariableValue(variable); if (styleValue == null) { try { colourVariation = getThemeChanger().getColorGroupMember(theme, variable); } catch (Exception e) { logger.log(Level.SEVERE, "Error while getting colour variation", e); } if (colourVariation != null) { styleValue = colourVariation.getColour(); } } if (styleValue == null) { styleValue = DEFAULT_COLOUR; } fixedCssExpression = computeCssValue(cssExpression, variable, styleValue); content = StringHandler.replace(content, cssExpression, fixedCssExpression); } return content; } private String replaceColourFileContent(String content, String variable, String value) { if (content == null) { return null; } if (variable == null || value == null) { return null; } int start = -1; int end = -1; String originalValue = null; String computedCSSValue = null; List<String> searchTerm = new ArrayList<String>(); searchTerm.add(CoreConstants.PERCENT); while (content.indexOf(variable) != -1) { start = getStartIndexForCssVariable(content, variable, CoreConstants.PERCENT); end = getEndIndexForCssVariable(content, variable, searchTerm, true, true); if (!canSubstring(content, start, end)) { logger.log(Level.WARNING, "Can not extract CSS expression '"+variable+"' because of invalid indexes: start index: " + start + ", end index: " + end + ". Using default colour: '"+DEFAULT_COLOUR+"'"); computedCSSValue = DEFAULT_COLOUR; // Error } originalValue = content.substring(start, end); computedCSSValue = computeCssValue(originalValue, variable, value); if (computedCSSValue == null) { logger.log(Level.WARNING, "Error occured computed CSS value for variable '"+variable+"', using default colour ('"+DEFAULT_COLOUR+"') for this variable."); computedCSSValue = DEFAULT_COLOUR; // Error } else if (computedCSSValue.length() != 4 && computedCSSValue.length() != 7) { int requiredLength = computedCSSValue.length() > 4 ? 7 : 4; while (computedCSSValue.length() < requiredLength) { computedCSSValue = computedCSSValue.replaceFirst(CoreConstants.HASH, String.valueOf(CoreConstants.HASH + 0)); } } content = StringHandler.replace(content, originalValue, computedCSSValue); } return content; } private String computeCssValue(String colourExpression, String variable, String value) { if (colourExpression.indexOf(CoreConstants.PLUS) == -1 && colourExpression.indexOf(CoreConstants.MINUS) == -1 && colourExpression.indexOf(CoreConstants.STAR) == -1) { // No math expression return value; } String finalHexExpression = null; colourExpression = colourExpression.toLowerCase(); colourExpression = StringHandler.replace(colourExpression, CoreConstants.PERCENT, CoreConstants.EMPTY); // Removing '%' colourExpression = StringHandler.replace(colourExpression, CoreConstants.SPACE, CoreConstants.EMPTY); // Removing white spaces colourExpression = StringHandler.replace(colourExpression, variable, value); // Replacing variable with value if (!isWithoutRGBExpression(colourExpression)) { colourExpression = getComputedHeximalValueByRGB(colourExpression); // RGB expression(s) will be replaced with hex number(s) } finalHexExpression = getComputedHeximalValueByCSSExpression(colourExpression); // -, +, * expression(s) return finalHexExpression == null ? value : finalHexExpression; } private String getComputedHeximalValueByRGB(String colourExpression) { if (colourExpression == null) { return null; } if (isWithoutRGBExpression(colourExpression)) { return colourExpression; } boolean roundResult = isNeedRoundResult(colourExpression); String redColourValue = extractRGBValue(colourExpression.substring(colourExpression.indexOf(RED_COLOUR_START) + RED_COLOUR_START.length())); if (redColourValue == null) { return null; } String greenColourValue = extractRGBValue(colourExpression.substring(colourExpression.indexOf(GREEN_COLOUR_START) + GREEN_COLOUR_START.length())); if (greenColourValue == null) { return null; } String blueColourValue = extractRGBValue(colourExpression.substring(colourExpression.indexOf(BLUE_COLOUR_START) + BLUE_COLOUR_START.length())); if (blueColourValue == null) { return null; } String MAX_VALUE = String.valueOf(255); String operation = CoreConstants.STAR; List<Integer> computedHexPartsInDecimals = new ArrayList<Integer>(3); makeMathOperation(MAX_VALUE, operation, redColourValue, computedHexPartsInDecimals, roundResult); makeMathOperation(MAX_VALUE, operation, greenColourValue, computedHexPartsInDecimals, roundResult); makeMathOperation(MAX_VALUE, operation, blueColourValue, computedHexPartsInDecimals, roundResult); String rgbReplacement = getConvertedDecimalsToHex(computedHexPartsInDecimals); if (rgbReplacement == null) { return null; } String fullRGBExpression = new StringBuffer(RED_COLOUR_START).append(redColourValue).append(ContentConstants.BRACKET_CLOSING).append(GREEN_COLOUR_START) .append(greenColourValue).append(ContentConstants.BRACKET_CLOSING).append(BLUE_COLOUR_START).append(blueColourValue) .append(ContentConstants.BRACKET_CLOSING).toString(); colourExpression = StringHandler.replace(colourExpression, fullRGBExpression, rgbReplacement); return getComputedHeximalValueByRGB(colourExpression); } private boolean isWithoutRGBExpression(String expression) { return expression.indexOf(RED_COLOUR_START) == -1 && expression.indexOf(GREEN_COLOUR_START) == -1 && expression.indexOf(BLUE_COLOUR_START) == -1; } private String extractRGBValue(String value) { int index = 0; String currentSymbol = value.substring(index, index + 1); while ((StringHandler.isNaturalNumber(currentSymbol) || CoreConstants.DOT.equals(currentSymbol)) && index + 1 < value.length()) { index++; currentSymbol = value.substring(index, index + 1); } String extractedValue = value.substring(0, index); return extractedValue; } private String getComputedHeximalValueByCSSExpression(String expression) { // Firstly making multiplication operations expression = makeMultiplication(expression); if (expression == null) { return null; } String variable = getHexValueFromExpression(expression); if (variable == null) { return null; } expression = StringHandler.replace(expression, variable, CoreConstants.EMPTY); String operand = null; List<String> operands = new ArrayList<String>(); while (expression.indexOf(CoreConstants.HASH) != -1) { operand = getHexValueFromExpression(expression); if (operand == null) { return null; } operands.add(operand); expression = StringHandler.replace(expression, operand, CoreConstants.EMPTY); } if (operands.isEmpty()) { return variable; } if (expression == null || expression.equals(CoreConstants.EMPTY)) { return null; } expression = expression.replaceAll(CoreConstants.SPACE, CoreConstants.EMPTY); if (expression.equals(CoreConstants.EMPTY) || expression.length() != operands.size()) { return null; } // Now left only operations: -, + int index = 0; String operation = null; String computedCSSValue = variable; while (expression.length() > 0) { operation = expression.substring(0, 1); if (operation.equals(CoreConstants.MINUS) || operation.equals(CoreConstants.PLUS)) { computedCSSValue = getExecutedColourOperation(computedCSSValue, operation, operands.get(index)); if (computedCSSValue == null) { return null; } } expression = expression.substring(1); index++; } return computedCSSValue; } private boolean isNeedRoundResult(String expression) { if (expression == null) { return false; } return expression.indexOf(CoreConstants.MINUS) != -1; } private String makeMultiplication(String cssColourExpression) { if (cssColourExpression == null) { return null; } if (cssColourExpression.indexOf(CoreConstants.STAR) == -1) { return cssColourExpression; } String expression = cssColourExpression.replaceAll(CoreConstants.SPACE, CoreConstants.EMPTY); if (expression.startsWith(CoreConstants.STAR) || expression.endsWith(CoreConstants.STAR)) { return null; } boolean roundResult = isNeedRoundResult(expression); int operationIndex = expression.indexOf(CoreConstants.STAR); String leftOperand = null; try { String leftPart = expression.substring(0, operationIndex); int indexFromTheEnd = 1; int leftPartLength = leftPart.length(); String currentSymbol = leftPart.substring(leftPartLength - indexFromTheEnd); while ((StringHandler.isNaturalNumber(currentSymbol) || CoreConstants.DOT.equals(currentSymbol) || SEARCH_TERMS.contains(currentSymbol)) && leftPartLength - indexFromTheEnd > 0) { indexFromTheEnd++; currentSymbol = leftPart.substring(leftPartLength - indexFromTheEnd, leftPartLength - indexFromTheEnd + 1); } leftOperand = leftPart.substring(leftPartLength - indexFromTheEnd); } catch(Exception e) { logger.log(Level.SEVERE, "Error getting left operand for multiplication operation", e); return null; } if (leftOperand == null) { return null; } String rightOperand = null; try { int index = 0; String rightPart = expression.substring(operationIndex + 1); String currentSymbol = rightPart.substring(index, index + 1); boolean firstOccuranceOfNumberSign = CoreConstants.HASH.equals(currentSymbol); while ((StringHandler.isNaturalNumber(currentSymbol) || CoreConstants.DOT.equals(currentSymbol) || firstOccuranceOfNumberSign || SEARCH_TERMS.contains(currentSymbol)) && !COLOUR_MATH_OPERATIONS.contains(currentSymbol) && index + 1 < rightPart.length()) { firstOccuranceOfNumberSign = false; index++; currentSymbol = rightPart.substring(index, index + 1); } int endIndex = COLOUR_MATH_OPERATIONS.contains(currentSymbol) ? index : index + 1; rightOperand = rightPart.substring(0, endIndex); } catch(Exception e) { logger.log(Level.SEVERE, "Error getting right operand for multiplication operation", e); return null; } if (rightOperand == null) { return null; } int startIndex = cssColourExpression.lastIndexOf(leftOperand); int endIndex = cssColourExpression.lastIndexOf(rightOperand) + rightOperand.length(); if (!canSubstring(cssColourExpression, startIndex, endIndex)) { return null; } String expressionToReplace = cssColourExpression.substring(startIndex, endIndex); if (expressionToReplace == null) { return null; } String computedValue = null; if (leftOperand.startsWith(CoreConstants.HASH) && rightOperand.startsWith(CoreConstants.HASH)) { // Both operands - hex numbers computedValue = getExecutedColourOperation(leftOperand, CoreConstants.STAR, rightOperand); } else { if (!leftOperand.startsWith(CoreConstants.HASH) && !rightOperand.startsWith(CoreConstants.HASH)) { // Both operand - decimal numbers Integer computedValueWithoutHexNumber = getComputedValue(expressionToReplace, roundResult); if (computedValueWithoutHexNumber == null) { return null; } computedValue = String.valueOf(computedValueWithoutHexNumber); } else { // One of operands is hex and another - decimal String[] splittedHexOperand = null; String decimalOperand = null; if (leftOperand.startsWith(CoreConstants.HASH)) { splittedHexOperand = getHexValueSplitted(leftOperand); decimalOperand = rightOperand; } else { splittedHexOperand = getHexValueSplitted(rightOperand); decimalOperand = leftOperand; } if (splittedHexOperand == null || decimalOperand == null) { return null; } List<Integer> hexPartsInDecimals = new ArrayList<Integer>(); for (String hexPart: splittedHexOperand) { makeMathOperation(hexPart, CoreConstants.STAR, decimalOperand, hexPartsInDecimals, roundResult); } computedValue = getConvertedDecimalsToHex(hexPartsInDecimals); } } if (computedValue == null) { return null; } cssColourExpression = StringHandler.replace(cssColourExpression, expressionToReplace, computedValue); return makeMultiplication(cssColourExpression); } private String getExecutedColourOperation(String operand1, String operation, String operand2) { String[] splitted1 = getHexValueSplitted(operand1); String[] splitted2 = getHexValueSplitted(operand2); if (splitted1 == null || splitted2 == null) { return null; } List<Integer> hexPartsInDecimals = new ArrayList<Integer>(); for (int i = 0; i <splitted1.length; i++) { makeMathOperation(splitted1[i], operation, splitted2[i], hexPartsInDecimals, false); } return getConvertedDecimalsToHex(hexPartsInDecimals); } private void makeMathOperation(String operand1, String operation, String operand2, List<Integer> computedHexPartsInDecimals, boolean roundResult) { Integer resultInDecimals = getComputedValue(getMathOperation(operand1, operation, operand2), roundResult); if (resultInDecimals == null) { return; } computedHexPartsInDecimals.add(resultInDecimals); } private Integer getComputedValue(String mathOperation, boolean roundResult) { if (mathOperation == null) { return null; } Object result = null; String cacheKey = roundResult ? new StringBuffer("rounded_").append(mathOperation).toString() : mathOperation; Integer resultInDecimals = HEX_TO_DEC_NUMBERS_CACHE.get(cacheKey); if (resultInDecimals == null) { try { result = mathInterpreter.eval(mathOperation); } catch(Exception e) { logger.log(Level.SEVERE, "Error while computing CSS colour value: " + mathOperation, e); return null; } if (result == null) { logger.log(Level.SEVERE, "Error while computing CSS colour value: " + mathOperation); return null; } if (result instanceof Double) { Double resultAsDouble = (Double) result; if (roundResult) { resultInDecimals = Long.valueOf(Math.round(resultAsDouble)).intValue(); } else { resultInDecimals = resultAsDouble.intValue(); } } else if (result instanceof Integer) { resultInDecimals = (Integer) result; } if (resultInDecimals == null) { return null; } HEX_TO_DEC_NUMBERS_CACHE.put(cacheKey, resultInDecimals); } return resultInDecimals; } private String getConvertedDecimalsToHex(List<Integer> hexPartsInDecimals) { if (hexPartsInDecimals == null || hexPartsInDecimals.isEmpty()) { return null; } String hexPartInString = null; StringBuffer computedHexValue = new StringBuffer(CoreConstants.HASH); for (Integer hexPart: hexPartsInDecimals) { if (hexPart < 0) { hexPart = 0; } if (hexPart > 255) { hexPart = 255; } hexPartInString = null; try { hexPartInString = Integer.toHexString(hexPart); } catch(NumberFormatException e) { logger.log(Level.SEVERE, "Error while converting decimal to hex: " + hexPart, e); return null; } if (hexPartInString == null) { return null; } if (hexPartInString.length() == 1) { hexPartInString = new StringBuffer("0").append(hexPartInString).toString(); } computedHexValue.append(hexPartInString); } return computedHexValue.toString(); } private String getMathOperation(String operand1, String operation, String operand2) { StringBuffer mathOperation = null; try { mathOperation = new StringBuffer().append(operand1.startsWith(CoreConstants.HASH) ? Integer.decode(operand1) : operand1).append(operation); mathOperation.append(operand2.startsWith(CoreConstants.HASH) ? Integer.decode(operand2) : operand2); } catch(NumberFormatException e) { logger.log(Level.SEVERE, "Error while converting hex value to decimal", e); return null; } return mathOperation == null ? null : mathOperation.toString(); } private String[] getHexValueSplitted(String hexValue) { if (hexValue.startsWith(CoreConstants.HASH)) { hexValue = StringHandler.replace(hexValue, CoreConstants.HASH, CoreConstants.EMPTY); } hexValue = hexValue.trim(); if (hexValue.length() != 3 && hexValue.length() != 6) { logger.log(Level.WARNING, "Invalid length (must be 3 or 6) for hex value: " + hexValue); return null; } String[] hexParts = new String[3]; int beginIndex = 0; int stepSize = hexValue.length() == 3 ? 1 : 2; int endIndex = stepSize; for (int i = 0; i < hexParts.length; i++) { if (!(canSubstring(hexValue, beginIndex, endIndex))) { logger.log(Level.WARNING, "Got invalid indexes (begin: " + beginIndex + ", end: " + endIndex + ") while splitting hex value: " + hexValue); return null; } hexParts[i] = new StringBuilder(CoreConstants.HASH).append(hexValue.substring(beginIndex, endIndex)).toString(); beginIndex += stepSize; endIndex += stepSize; } return hexParts; } private String getHexValueFromExpression(String expression) { int start = getStartIndexForCssVariable(expression, CoreConstants.HASH, CoreConstants.HASH); int end = getEndIndexForCssVariable(expression, CoreConstants.HASH, SEARCH_TERMS, false, false); if (!(canSubstring(expression, start, end))) { return null; // Error } return expression.substring(start, end); } private boolean canSubstring(String content, int start, int end) { if (content == null || start == -1 || end == -1) { return false; } if (start < 0 || end <= start || end > content.length()) { return false; } return true; } private int getEndIndexForCssVariable(String content, String key, List<String> searchTerms, boolean checkIfContains, boolean increaseIndex) { int index = content.indexOf(key); if (index == -1) { return -1; } index += key.length(); boolean foundPercentMarkAtTheEnd = false; while (!foundPercentMarkAtTheEnd && index < content.length()) { if (checkIfContains) { if (searchTerms.contains(content.substring(index, index + 1))) { foundPercentMarkAtTheEnd = true; } else { index++; } } else { if (!(searchTerms.contains(content.substring(index, index + 1)))) { foundPercentMarkAtTheEnd = true; } else { index++; } } } if (increaseIndex) { if (index + 1 < content.length()) { index++; } } return index; } private int getStartIndexForCssVariable(String content, String key, String searchTerm) { int index = content.indexOf(key); if (index == -1) { return -1; } boolean foundPercentMarkAtTheBegin = false; while (!foundPercentMarkAtTheBegin && index > 0) { if (content.substring(index).startsWith(searchTerm)) { foundPercentMarkAtTheBegin = true; } else { index--; } } return index; } public ThemesHelper getHelper() { return helper; } public void setHelper(ThemesHelper helper) { this.helper = helper; } public ThemeChanger getThemeChanger() { return themeChanger; } public void setThemeChanger(ThemeChanger themeChanger) { this.themeChanger = themeChanger; } }