/* * * 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.apache.flex.compiler.internal.resourcebundles; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.Reader; import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.flex.compiler.common.ISourceLocation; import org.apache.flex.compiler.common.SourceLocation; import org.apache.flex.compiler.constants.IMetaAttributeConstants; import org.apache.flex.compiler.internal.parsing.as.ASParser; import org.apache.flex.compiler.internal.tree.as.ClassReferenceNode; import org.apache.flex.compiler.internal.tree.as.EmbedNode; import org.apache.flex.compiler.internal.tree.as.ExpressionNodeBase; import org.apache.flex.compiler.internal.tree.as.LiteralNode; import org.apache.flex.compiler.internal.tree.as.metadata.MetaTagsNode; import org.apache.flex.compiler.internal.tree.properties.ResourceBundleEntryNode; import org.apache.flex.compiler.internal.tree.properties.ResourceBundleFileNode; import org.apache.flex.compiler.problems.FileNotFoundProblem; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.problems.InternalCompilerProblem2; import org.apache.flex.compiler.problems.ParserProblem; import org.apache.flex.compiler.problems.ResourceBundleMalformedEncodingProblem; import org.apache.flex.compiler.tree.as.ILiteralNode.LiteralType; import org.apache.flex.compiler.tree.metadata.IMetaTagNode; import org.apache.flex.compiler.workspaces.IWorkspace; /** * Properties parser that reads a properties file in Unicode. */ public class PropertiesFileParser { public static final Pattern CLASS_REFERENCE_REGEX = Pattern.compile("ClassReference\\((.*)\\)"); public static final Pattern EMBED_REGEX = Pattern.compile(IMetaAttributeConstants.ATTRIBUTE_EMBED + "\\(.*\\)"); /** * Characters we skip in certain places */ private static final String WHITESPACE = " \t\n\r\f"; /** * Characters that terminate a key */ private static String SPLITTERS = "=: \t"; /** * Characters that terminate a value */ private static final String TERMINATORS = "\n\r\f"; /** * Path of the file that is parsed. */ private String filePath; /** * File node for the properties file being parsed */ private ResourceBundleFileNode fileNode; /** * Collection that is used to store problems that occur during parsing. */ private Collection<ICompilerProblem> problems; private IWorkspace workspace; /** * Constructor */ public PropertiesFileParser(IWorkspace workspace) { this.workspace = workspace; } /** * This method attempts to parse a .properties file * using the same rules as Java, except that the file * is assumed to have UTF-8 encoding. * * Let <ow> indicates optional whitespace and <rw> required whitespace. * * Comment lines have the form <ow>#<comment> or <ow>!<comment> * If # or ! isn't the first non-whitespace character on a line, * it doesn't start a comment. * * Key/value pairs have the form <ow>key<ow>=<ow>value * or <ow>key<ow>:<ow>value or <ow>key<rw>value * In other words, you can use an equal sign, a colon, * or just whitespace to separate the key from the value. * * Trailing whitespace is not stripped from the value. * * You can use standard escape sequences * like \n, \r, \t, \u1234, and \\. * * Backslash-space is an escape sequence for a space; * for example, if a value needs to start with a space * you must write it as backslash-space or it will be * interpreted as optional whitespace preceding the value. * However, you don't need to escape spaces within a value. * * You can continue a line by ending it with a backslash. * Leading whitespace on the next line is stripped. * * Backslashes that aren't part of an escape sequence are removed. * For example, \A is just A. * * You don't need to escape a double-quote or a single-quote * (but it doesn't hurt to do so). * * @param filePath path of the properties file to parse. * @param locale locale of the file if it is locale dependent, otherweise <code>null</code> * @param reader reader that wraps an open stream to the file to parse. * @param problems collection that is used to store problems that occur * during parsing */ public ResourceBundleFileNode parse(String filePath, String locale, Reader reader, Collection<ICompilerProblem> problems) { this.filePath = filePath; this.problems = problems; try { fileNode = new ResourceBundleFileNode(workspace, filePath, locale); parse(new BufferedReader(reader)); return fileNode; } catch(FileNotFoundException ex) { ICompilerProblem problem = new FileNotFoundProblem(filePath); problems.add(problem); } catch (IOException ex) { ICompilerProblem problem = new InternalCompilerProblem2(filePath, ex, "PropertiesFileParser"); problems.add(problem); } finally { if (reader != null) { try { reader.close(); } catch (IOException ex) { } } } return null; } /** * Parses the properties file. * * @param br buffered reader to read the properties file. * @throws IOException */ private void parse(BufferedReader br) throws IOException { String line; StringBuilder buffer = new StringBuilder(100); int lineNumber = 0; int comment_length=0; String sep = System.getProperty("line.separator"); int sep_len = sep.length(); int offset = 0; while((line=br.readLine())!=null) { lineNumber++; int len = line.length(); offset+=len; int start=0; // TODO: Clean this up some day by using: // http://commons.apache.org/io/api-release/org/apache/commons/io/input/BOMInputStream.html // skip the Unicode BOM; UTF-8 is indicated by the byte sequence // EF BB BF, which is the UTF-8 encoding of the character U+FEFF) if (lineNumber == 1 && len > 0 && line.charAt(0) == '\uFEFF') { line = line.substring(1); len = line.length(); offset = len; } // find first non-whitespace char for(;start<len && WHITESPACE.indexOf(line.charAt(start))!=-1;start++); if (line.trim().length() == 0) { buffer.append(sep); comment_length+=sep_len; continue; } // if lines starts with !, # or only contains whitespace // add it to the buffer and start over with a new line if(len==0 || line.charAt(start)=='!' || line.charAt(start)=='#' || WHITESPACE.indexOf(line.charAt(start))!=-1) { buffer.append(line); buffer.append(sep); comment_length+=len+sep_len; continue; } // done with comment save it if(comment_length!=0) { buffer.setLength(comment_length); } buffer.setLength(0); // put start of name=value piece into beginning of buffer buffer.append(line.substring(start)); offset+=start; // a line ending with a backslash is continued onto the following line while(line != null && line.length() > 1 && line.charAt(line.length()-1)=='\\') { buffer.setLength(buffer.length()-1); // remove the backslash line=br.readLine(); if(line!=null) { int new_start = 0; len = line.length(); // find first non-whitespace char for(;new_start < len && WHITESPACE.indexOf(line.charAt(new_start))!=-1; new_start++); // add to buffer buffer.append(line.substring(new_start)); } } String propLine = buffer.toString(); String com_key = loadProperty(propLine, lineNumber, offset, start); if(comment_length!=0 && com_key != null) { comment_length=0; } buffer.setLength(0); } } /** * Parses a line in a property file. * * @param prop * @param lineNumber * @return */ private String loadProperty(String property, int lineNumber, int startOffset, int column) { String key; String value; int prop_len=property.length(); int prop_index=0; // key for(; prop_index<prop_len; prop_index++) { char current = property.charAt(prop_index); if(current == '\\') prop_index++; else if(SPLITTERS.indexOf(current) != -1) break; } key = property.substring(0, prop_index); key = unescape(key, startOffset, column, lineNumber, property); key = key.trim(); // got key now go to first non-whitespace for(; prop_index<property.length() && WHITESPACE.indexOf(property.charAt(prop_index))!=-1; prop_index++); try { // also skip : or = if(property.charAt(prop_index)==':' || property.charAt(prop_index)=='=') { prop_index++; // skip any more whitespace for(; prop_index<property.length() && WHITESPACE.indexOf(property.charAt(prop_index))!=-1; prop_index++); } } catch (StringIndexOutOfBoundsException ex) { return null; } int value_start=prop_index; // read value for(;prop_index<property.length(); prop_index++) { char current = property.charAt(prop_index); if(current == '\\') prop_index++; else if(TERMINATORS.indexOf(current) != -1) break; } value = property.substring(value_start,prop_index); value = unescape(value, startOffset+value_start, value_start, lineNumber, property); SourceLocation keyLocation = new SourceLocation(filePath, startOffset, startOffset+key.length(), lineNumber, column); SourceLocation valueLocation = new SourceLocation(filePath, startOffset+value_start, startOffset+value_start+value.length(), lineNumber, value_start); process(key, value, keyLocation, valueLocation, problems); return key; } /** * Do opposite of escape on a given string. * * @param string string that contains characters we want to un-escape * @return un-escaped string */ private String unescape(String string, int start, int column, int line, String lineText) { if(string==null) return null; StringBuilder buffer = new StringBuilder(string.length()); int string_index=0; while(string_index < string.length()) { char add = string.charAt(string_index++); if(add == '\\') { add = string.charAt(string_index++); // handle unicode chars, else escaped single chars if(add == 'u') { // Read the xxxx int unicode=0; for (int i=0; i<4; i++) { add = string.charAt(string_index++); switch (add) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': unicode = (unicode << 4) + add - '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': unicode = (unicode << 4) + 10 + add - 'a'; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': unicode = (unicode << 4) + 10 + add - 'A'; break; default: { ISourceLocation location = new SourceLocation(filePath, start, start + string.length(), line, column); ICompilerProblem problem = new ResourceBundleMalformedEncodingProblem(location, string); problems.add(problem); } } } add = (char) unicode; } else { // add escaped char to value switch(add) { case 't': add = '\t'; break; case 'n': add = '\n'; break; case 'r': add = '\r'; break; case 'f': add = '\f'; break; } } buffer.append(add); } else buffer.append(add); } return buffer.toString(); } /** * Process a key-value pair extracted from a properties file while parsing. * * @param key key of the property * @param value value of the property * @param keySource location information of key * @param valueSource location information of value * @param problems collection storing problems that occur during parsing */ private void process(String key, String value, SourceLocation keySource, SourceLocation valueSource, Collection<ICompilerProblem> problems) { LiteralNode keyNode = new LiteralNode(LiteralType.STRING, key, keySource); ExpressionNodeBase valueNode = null; Matcher matcher; if ((matcher = CLASS_REFERENCE_REGEX.matcher(value)).matches()) { valueNode = processClassReference(matcher, valueSource, problems); } else if ((matcher = EMBED_REGEX.matcher(value)).matches()) { valueNode = processEmbed(value, valueSource, problems); } else { valueNode = new LiteralNode(LiteralType.STRING, value, valueSource); } if(valueNode != null) fileNode.addItem(new ResourceBundleEntryNode(keyNode, valueNode)); } /** * Process a ClassReference directive that occurs in a properties file. * * @param matcher matcher that has already identified a ClassReference directive in a value. * @param sourceLocation location where this directive occurred in the file * @param problems collection to add problems if encountered during * processing. * @return a {@link ClassReferenceNode} instance that represents this * occurrence or <code>null</code> if any problem occurs. */ private ClassReferenceNode processClassReference(Matcher matcher, SourceLocation sourceLocation, Collection<ICompilerProblem> problems) { try { String qName = matcher.group(1).trim(); if (qName.equals("null")) { return new ClassReferenceNode(null, sourceLocation); } if ((qName.charAt(0) == '"') && (qName.indexOf('"', 1) == qName.length() - 1)) { qName = qName.substring(1, qName.length() - 1); return new ClassReferenceNode(qName, sourceLocation); } } catch(Exception e) { //do nothing, problem will be reported next. } ParserProblem problem = new ParserProblem(sourceLocation); problems.add(problem); return null; } /** * Process a Embed directive that occurs in a properties file. * * @param value Embed directive to process * @param sourceLocation location where this directive occurred in the file * @param problems collection to add problems if encountered during * processing. * @return a {@link EmbedNode} instance that represents this * occurrence or <code>null</code> if any problem occurs. */ private EmbedNode processEmbed(String value, SourceLocation sourceLocation, Collection<ICompilerProblem> problems) { StringBuilder sb = new StringBuilder(); sb.append("["); sb.append(value); sb.append("]"); MetaTagsNode metaTagsNode = ASParser.parseMetadata( workspace, sb.toString(), sourceLocation.getSourcePath(), sourceLocation.getAbsoluteStart(), sourceLocation.getLine(), sourceLocation.getColumn(), problems); if (metaTagsNode == null) return null; IMetaTagNode embedMetaTagNode = metaTagsNode.getTagByName(IMetaAttributeConstants.ATTRIBUTE_EMBED); if (embedMetaTagNode == null) return null; EmbedNode embedNode = new EmbedNode(filePath, embedMetaTagNode, fileNode); embedNode.setSourceLocation(sourceLocation); return embedNode; } }