| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.web.el; |
| |
| import com.sun.el.parser.ELParserConstants; |
| import com.sun.el.parser.ELParserTokenManager; |
| import com.sun.el.parser.SimpleCharStream; |
| import com.sun.el.parser.Token; |
| import java.io.StringReader; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import org.netbeans.modules.csl.api.OffsetRange; |
| |
| /** |
| * Converts expression language expressions with respect to the given conversion table. |
| * Allows to convert offset between the original and converted expressions. |
| * Typical usage is conversion of the xml entity references inside the facelets expressions. |
| * |
| * XXX: only shortening patterns (XX->Y) are supported so not rule like (A->BB) will work. |
| * |
| * @todo Make the class pluggable so the xml - jsf entities are not hardcoded. |
| * |
| * @author marekfukala |
| */ |
| public class ELPreprocessor { |
| |
| private static final Logger LOG = Logger.getLogger(ELPreprocessor.class.getName()); |
| |
| /** |
| * Html entity references conversion table. |
| */ |
| public static final String[][] XML_ENTITY_REFS_CONVERSION_TABLE = new String[][]{ //NOI18N |
| {"&", "&"}, |
| {">", ">"}, |
| {"<", "<"}, |
| {""", "\""}, |
| {"'", "'"} |
| }; |
| |
| //escaped chars conversion for attribute values |
| public static final String[][] ESCAPED_CHARACTERS = new String[][]{ |
| {"\\\\", "\\"}, /* \\ -> \ */ |
| {"\\<", "<"}, /* \< -> < */ |
| {"\\>", ">"}, /* \> -> > */ |
| {"\\\"", "\""}, /* \" -> " */ |
| {"\\'", "'"}, /* \' -> ' */ |
| {"\\&", "&"}, /* \& -> & */ |
| }; |
| |
| private final String originalExpression; |
| private final String[][][] conversionTables; |
| |
| private String preprocessedExpression; |
| private final boolean[] diffs; |
| |
| private List<OffsetRange> stringLiterals = new LinkedList<>(); |
| |
| public ELPreprocessor(String expression, String[][]... conversionTables) { |
| this.originalExpression = expression; |
| this.conversionTables = conversionTables; |
| this.diffs = new boolean[originalExpression.length()]; |
| init(); |
| } |
| |
| public String getOriginalExpression() { |
| return originalExpression; |
| } |
| |
| public String getPreprocessedExpression() { |
| return preprocessedExpression; |
| } |
| |
| public int getOriginalOffset(int preprocessedELoffset) { |
| int diff = 0; |
| for(int i = 0; i < originalExpression.length(); i++) { |
| int pointer = i + diff; |
| if(pointer == preprocessedELoffset) { |
| return i; |
| } |
| diff += diffs[i] ? -1 : 0; |
| } |
| //if we got here the offset points at the very end of the expression |
| assert preprocessedELoffset == preprocessedExpression.length(); //or there's a bug |
| return originalExpression.length(); //last offset handled here |
| } |
| |
| public int getPreprocessedOffset(int originalOffset) { |
| int diff = 0; |
| for(int i = 0; i < originalOffset; i++) { |
| diff += diffs[i] ? -1 : 0; |
| } |
| return originalOffset + diff; |
| } |
| |
| //the algorithm is far from the most effective one, but for relatively small |
| //set of the patterns the complexity is acceptable |
| private void init() { |
| long start = System.nanoTime(); |
| boolean[] localDiffs = new boolean[diffs.length]; |
| String result = originalExpression; |
| preprocessStringLiterals(); |
| LOG.log(Level.FINEST, "StringLiteral preprocessing took: {0} ns", (System.nanoTime() - start)); |
| for(String[][] table : conversionTables) { |
| for(String[] patternPair : table) { |
| //create local diffs copy - needs to be used to properly convert positions during the processing |
| System.arraycopy(diffs, 0, localDiffs, 0, diffs.length); |
| |
| StringBuilder resolved = new StringBuilder(); |
| String source = patternPair[0]; |
| String dest = patternPair[1]; |
| |
| assert source.length() >= dest.length() : "no expanding rules supported!"; //NOI18N |
| |
| int match; |
| int lastMatchEnd = 0; |
| while((match = result.indexOf(source, lastMatchEnd)) != -1) { |
| if (isInsideStringLiteral(match)) { |
| resolved.append(result.substring(lastMatchEnd, match)); |
| resolved.append(source); |
| lastMatchEnd = match + source.length(); |
| continue; |
| } |
| resolved.append(result.substring(lastMatchEnd, match)); |
| resolved.append(dest); |
| int originalSourceMatch = getOriginalOffset(match); //we operate on the already modified source text |
| int patternsLenDiff = source.length() - dest.length(); |
| for(int i = originalSourceMatch; i < originalSourceMatch + patternsLenDiff; i++) { |
| localDiffs[i] = true; |
| } |
| |
| lastMatchEnd = match + source.length(); |
| } |
| resolved.append(result.substring(lastMatchEnd)); |
| |
| result = resolved.toString(); |
| |
| //set the locally modified diffs to the original diff array |
| //this copying is necessary so the getOriginalOffset() conversion works properly *during* the |
| //actual source conversion where |
| System.arraycopy(localDiffs, 0, diffs, 0, diffs.length); |
| } |
| |
| } |
| this.preprocessedExpression = result; |
| LOG.log(Level.FINEST, "All preprocessing took: {0} ns", (System.nanoTime() - start)); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(ELPreprocessor.class.getSimpleName()); |
| sb.append('('); |
| sb.append(System.identityHashCode(this)); |
| sb.append(')'); |
| sb.append('\n'); |
| for(String[][] table : conversionTables) { |
| sb.append("table:"); |
| for(String[] pattern : table) { |
| sb.append('('); |
| sb.append(pattern[0]); |
| sb.append("->"); |
| sb.append(pattern[1]); |
| sb.append(')'); |
| } |
| sb.append(' '); |
| } |
| sb.append('\n'); |
| sb.append("source:"); |
| sb.append(getOriginalExpression()); |
| sb.append("\n"); |
| sb.append("diffs :"); |
| for(int i = 0; i < diffs.length; i++) { |
| sb.append(diffs[i] ? "-" : "0"); |
| } |
| sb.append("\n"); |
| sb.append("result:"); |
| sb.append(getPreprocessedExpression()); |
| sb.append("\n"); |
| |
| return sb.toString(); |
| } |
| |
| private void preprocessStringLiterals() { |
| // optimalization - epressions w/out any string mustn't be preprocessed |
| if (!originalExpression.contains("'") && !originalExpression.contains("\"")) { //NOI18N |
| return; |
| } |
| SimpleCharStream simpleCharStream = new SimpleCharStream(new StringReader(originalExpression)); |
| ELParserTokenManager tokenManager = new ELParserTokenManager(simpleCharStream); |
| while (true) { |
| Token t = tokenManager.getNextToken(); |
| if (t.kind == ELParserConstants.EOF) { |
| break; |
| } else if (t.kind == ELParserConstants.STRING_LITERAL) { |
| stringLiterals.add(new OffsetRange(t.beginColumn, t.beginColumn + t.image.length() - 1)); |
| } |
| } |
| } |
| |
| private boolean isInsideStringLiteral(int offset) { |
| for (OffsetRange offsetRange : stringLiterals) { |
| if (offsetRange.getStart() <= offset && offsetRange.getEnd() > offset) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } |