blob: 9c7624aeea7ab74230d5664fdac2d3839ed4e526 [file] [log] [blame]
/*
* 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.properties;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Position;
import org.openide.text.PositionRef;
import org.openide.text.PositionBounds;
/**
* Parser of .properties files. It generates structure of comment-key-vaue property elements.
*
* @author Petr Jiricka, Petr Hamernik, Peter Zavadsky
* @see PropertiesStructure
* @see Element.ItemElem
*/
class PropertiesParser {
/** PropertiesFileEntry for which source is this parser created. */
PropertiesFileEntry pfe;
/** Appropriate properties editor - used for creating the PositionRefs */
PropertiesEditorSupport editor;
/** Properties file reader. Input stream. */
PropertiesReader propertiesReader;
/** Flag if parsing should be stopped. */
private boolean stop = false;
/**
* Creates parser. Has to be {@link init} afterwards.
* @param pfe FileEntry where the properties file is stored.
*/
public PropertiesParser(PropertiesFileEntry pfe) {
this.pfe = pfe;
}
/** Inits parser.
* @exception IOException if any i/o problem occured during reading */
void initParser() throws IOException {
editor = pfe.getPropertiesEditor();
propertiesReader = createReader();
}
/** Creates new input stream from the file object.
* Finds the properties data object, checks if the document is loaded,
* if not is loaded and created a stream from the document.
* @exception IOException if any i/o problem occured during reading
*/
private PropertiesReader createReader() throws IOException {
// Get loaded document, or load it if necessary.
Document loadDoc = null;
if(editor.isDocumentLoaded()) {
loadDoc = editor.getDocument();
}
if(loadDoc == null) {
loadDoc = editor.openDocument();
}
final Document document = loadDoc;
final String[] str = new String[1];
// safely take the text from the document
document.render(new Runnable() {
public void run() {
try {
str[0] = document.getText(0, document.getLength());
} catch(BadLocationException ble) {
// Should be not possible.
ble.printStackTrace();
}
}
});
return new PropertiesReader(str[0]);
}
/** Parses .properties file specified by <code>pfe</code> and resets its properties
* structure.
* @return new properties structure or null if parsing failed
*/
public PropertiesStructure parseFile() {
try {
PropertiesStructure propStructure = parseFileMain();
return propStructure;
} catch(IOException e) {
// Parsing failed, return null.
return null;
}
}
/** Stops parsing. */
public void stop() {
stop = true;
clean();
}
/** Provides clean up after finish parsing. */
public void clean() {
if(propertiesReader != null) {
try {
propertiesReader.close();
propertiesReader = null;
} catch(IOException ioe) {
org.openide.ErrorManager.getDefault().notify(org.openide.ErrorManager.INFORMATIONAL, ioe);
}
}
}
/** Parses .properties file and creates <code>PropertiesStruture</code>. */
private PropertiesStructure parseFileMain() throws IOException {
Map<String,Element.ItemElem> items = new HashMap<String,Element.ItemElem>(25, 1.0F);
PropertiesReader reader = null;
while (true) {
if (stop) {
// Parsing stopped -> return immediatelly.
return null;
}
reader = propertiesReader;
if (reader == null) {
// Parsing was stopped.
return null;
}
Element.ItemElem element = readNextElem(reader);
if (element == null) {
break;
} else {
// add at the end of the list
items.put(element.getKey(), element);
}
}
return new PropertiesStructure(createBiasBounds(0, reader.position), items);
}
/**
* Reads next element from input stream.
* @return next element or null if the end of the stream occurred */
private Element.ItemElem readNextElem(PropertiesReader in) throws IOException {
Element.CommentElem commE;
Element.KeyElem keyE;
Element.ValueElem valueE;
int begPos = in.position;
// read the comment
int keyPos = begPos;
FlaggedLine fl = in.readLineExpectComment();
StringBuffer comment = new StringBuffer();
boolean firstNull = true;
while (fl != null) {
firstNull = false;
if(fl.flag) {
//part of the comment
comment.append(trimComment(fl.line));
comment.append(fl.lineSep);
keyPos = in.position;
} else
// not a part of a comment
break;
fl = in.readLineExpectComment();
}
// exit completely if null is returned the very first time
if (firstNull) {
return null;
}
String comHelp;
comHelp = comment.toString();
if(comment.length() > 0)
if(comment.charAt(comment.length() - 1) == '\n')
comHelp = comment.substring(0, comment.length() - 1);
commE = new Element.CommentElem(createBiasBounds(begPos, keyPos), UtilConvert.loadConvert(comHelp));
// fl now contains the line after the comment or null if none exists
if(fl == null) {
keyE = null;
valueE = null;
} else {
// read the key and the value
// list of
ArrayList<FlaggedLine> lines = new ArrayList<FlaggedLine>(2);
fl.startPosition = keyPos;
fl.stringValue = fl.line.toString();
lines.add(fl);
int nowPos;
while (isPartialLine(fl.line)) {
// do something with the previous line
fl.stringValue = fl.stringValue.substring(0, fl.stringValue/*fix: was: line*/.length() - 1);
// now the new line
nowPos = in.position;
fl = in.readLineNoFrills();
if(fl == null) break;
// delete the leading whitespaces
int startIndex=0;
for(startIndex=0; startIndex < fl.line.length(); startIndex++)
if(UtilConvert.whiteSpaceChars.indexOf(fl.line.charAt(startIndex)) == -1)
break;
fl.stringValue = fl.line.substring(startIndex, fl.line.length());
fl.startPosition = nowPos + startIndex;
lines.add(fl);
}
// now I have an ArrayList with strings representing lines and positions of the first non-whitespace character
PositionMap positionMap = new PositionMap(lines);
String line = positionMap.getString();
// Find start of key
int len = line.length();
int keyStart;
for(keyStart=0; keyStart<len; keyStart++) {
if(UtilConvert.whiteSpaceChars.indexOf(line.charAt(keyStart)) == -1)
break;
}
// Find separation between key and value
int separatorIndex;
for(separatorIndex=keyStart; separatorIndex<len; separatorIndex++) {
char currentChar = line.charAt(separatorIndex);
if(currentChar == '\\')
separatorIndex++;
else if(UtilConvert.keyValueSeparators.indexOf(currentChar) != -1)
break;
}
// Skip over whitespace after key if any
int valueIndex;
for (valueIndex=separatorIndex; valueIndex<len; valueIndex++)
if(UtilConvert.whiteSpaceChars.indexOf(line.charAt(valueIndex)) == -1)
break;
// Skip over one non whitespace key value separators if any
if(valueIndex < len)
if(UtilConvert.strictKeyValueSeparators.indexOf(line.charAt(valueIndex)) != -1)
valueIndex++;
// Skip over white space after other separators if any
while (valueIndex < len) {
if(UtilConvert.whiteSpaceChars.indexOf(line.charAt(valueIndex)) == -1)
break;
valueIndex++;
}
String key = line.substring(keyStart, separatorIndex);
String value = (separatorIndex < len) ? line.substring(valueIndex, len) : ""; // NOI18N
if(key == null)
// PENDING - should join with the next comment
;
int currentPos = in.position;
int valuePosFile = 0;
try {
valuePosFile = positionMap.getFilePosition(valueIndex);
} catch (ArrayIndexOutOfBoundsException e) {
valuePosFile = currentPos;
}
keyE = new Element.KeyElem (createBiasBounds(keyPos, valuePosFile), UtilConvert.loadConvert(key));
valueE = new Element.ValueElem(createBiasBounds(valuePosFile, currentPos), UtilConvert.loadConvert(value));
}
return new Element.ItemElem(createBiasBounds(begPos, in.position), keyE, valueE, commE);
}
/** Remove leading comment markers. */
private StringBuffer trimComment(StringBuffer line) {
while (line.length() > 0) {
char lead = line.charAt(0);
if (lead == '#' || lead == '!') {
line.deleteCharAt(0);
} else {
break;
}
}
return line;
}
/** Utility method. Computes the real offset from the long value representing position in the parser.
* @return the offset
*/
private static int position(long p) {
return (int)(p & 0xFFFFFFFFL);
}
/** Creates position bounds. For obtaining the real offsets is used
* previous method position()
* @param begin the begin in the internal position form
* @param end the end in the internal position form
* @return the bounds
*/
private PositionBounds createBiasBounds(long begin, long end) {
PositionRef posBegin = editor.createPositionRef(position(begin), Position.Bias.Forward);
PositionRef posEnd = editor.createPositionRef(position(end), Position.Bias.Backward);
return new PositionBounds(posBegin, posEnd);
}
/**
* Properties reader which allows reading from an input stream or from a string and remembers
* its position in the document.
*/
private static class PropertiesReader extends BufferedReader {
/** Name constant of line separator system property. */
private static final String LINE_SEPARATOR = "line.separator"; // NOI18N
/** The character that someone peeked. */
private int peekChar = -1;
/** Position after the last character read. */
public int position = 0;
/** Creates <code>PropertiesReader</code> from buffer. */
private PropertiesReader(String buffer) {
super(new StringReader(buffer));
}
/** Creates <code>PropertiesReader</code> from another reader. */
private PropertiesReader(Reader reader) {
super(reader);
}
/** Read one character from the stream and increases the position.
* @return the character or -1 if the end of the stream has been reached
*/
public int read() throws IOException {
int character = peek();
peekChar = -1;
if(character != -1)
position++;
return character;
}
/** Returns the next character without increasing the position. Subsequent calls
* to peek() and read() will return the same character.
* @return the character or -1 if the end of the stream has been reached
*/
private int peek() throws IOException {
if(peekChar == -1)
peekChar = super.read();
return peekChar;
}
/** Reads the next line and returns the flag as true if the line is a comment line.
* If the input is empty returns null
* Flag in the result is true if the line is a comment line
*/
public FlaggedLine readLineExpectComment() throws IOException {
int charRead = read();
if(charRead == -1)
// end of the reader reached
return null;
boolean decided = false;
FlaggedLine fl = new FlaggedLine();
while (charRead != -1 && charRead != (int)'\n' && charRead != (int)'\r') {
if(!decided)
if(UtilConvert.whiteSpaceChars.indexOf((char)charRead) == -1) {
// not a whitespace - decide now
fl.flag = (((char)charRead == '!') || ((char)charRead == '#'));
decided = true;
}
fl.line.append((char)charRead);
charRead = read();
}
if(!decided)
// all were whitespaces
fl.flag = true;
// set the line separator
if(charRead == (int)'\r')
if(peek() == (int)'\n') {
charRead = read();
fl.lineSep = "\r\n"; // NOI18N
} else
fl.lineSep = "\r"; // NOI18N
else
if(charRead == (int)'\n')
fl.lineSep = "\n"; // NOI18N
else
fl.lineSep = System.getProperty(LINE_SEPARATOR);
return fl;
}
/** Reads the next line.
* @return <code>FlaggedLine</code> or null if the input is empty */
public FlaggedLine readLineNoFrills() throws IOException {
int charRead = read();
if(charRead == -1)
// end of the reader reached
return null;
FlaggedLine fl = new FlaggedLine();
while (charRead != -1 && charRead != (int)'\n' && charRead != (int)'\r') {
fl.line.append((char)charRead);
charRead = read();
}
// set the line separator
if(charRead == (int)'\r')
if(peek() == (int)'\n') {
charRead = read();
fl.lineSep = "\r\n"; // NOI18N
} else
fl.lineSep = "\r"; // NOI18N
else
if(charRead == (int)'\n') // NOI18N
fl.lineSep = "\n"; // NOI18N
else
fl.lineSep = System.getProperty(LINE_SEPARATOR);
return fl;
}
} // End of nested class PropertiesReader.
/**
* Returns true if the given line is a line that must
* be appended to the next line
*/
private static boolean isPartialLine (StringBuffer line) {
int slashCount = 0;
int index = line.length() - 1;
while((index >= 0) && (line.charAt(index--) == '\\'))
slashCount++;
return (slashCount % 2 == 1);
}
/** Nested class which maps positions in a string to positions in the underlying file.
* @see FlaggedLine */
private static class PositionMap {
/** List of <code>FlaggedLine</code>'s. */
private List<FlaggedLine> list;
/** Constructor - expects a list of FlaggedLine */
PositionMap(List<FlaggedLine> lines) {
list = lines;
}
/** Returns the string represented by the object */
public String getString() {
String allLines = list.get(0).stringValue;
for (int part=1; part<list.size(); part++) {
allLines += list.get(part).stringValue;
}
return allLines;
}
/** Returns position in the file for a position in a string
* @param posString position in the string to find file position for
* @return position in the file
* @exception ArrayIndexOutOfBoundsException if the requested position is outside
* the area represented by this object
*/
public int getFilePosition(int posString) throws ArrayIndexOutOfBoundsException {
// get the part
int part;
int lengthSoFar = 0;
int lastLengthSoFar = 0;
for (part=0; part < list.size(); part++) {
lastLengthSoFar = lengthSoFar;
lengthSoFar += list.get(part).stringValue.length();
// brute patch - last (cr)lf should not be the part of the thing, other should
if (part == list.size() - 1) {
if (lengthSoFar >= posString) {
break;
}
} else {
if (lengthSoFar > posString) {
break;
}
}
}
if (posString > lengthSoFar) {
throw new ArrayIndexOutOfBoundsException("not in scope"); // NOI18N
}
return list.get(part).startPosition + posString - lastLengthSoFar;
}
} // End of nested class PositionMap.
/** Helper nested class. */
private static class FlaggedLine {
/** Line buffer. */
StringBuffer line;
/** Flag. */
boolean flag;
/** Line separator. */
String lineSep;
/** Start position. */
int startPosition;
/** Value. */
String stringValue;
/** Constructor. */
FlaggedLine() {
line = new StringBuffer();
flag = false;
lineSep = "\n"; // NOI18N
startPosition = 0;
}
} // End of nested class FlaggedLine.
}