| /* |
| * 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.beans.*; |
| import java.io.*; |
| import javax.swing.text.BadLocationException; |
| |
| import org.openide.nodes.Node; |
| import org.openide.ErrorManager; |
| import org.openide.text.PositionBounds; |
| |
| |
| /** |
| * Base class for representations of elements in properties files. |
| * |
| * @author Petr Jiricka |
| * @author Petr Kuzel - moved to nonescaped strings level |
| * //!!! why is it serializable? |
| */ |
| public abstract class Element implements Serializable { |
| |
| /** Property change support */ |
| private transient PropertyChangeSupport support = new PropertyChangeSupport(this); |
| |
| /** Position of the begin and the end of the element. Could |
| * be null indicating the element is not part of properties structure yet. */ |
| protected PositionBounds bounds; |
| |
| |
| /** Create a new element. */ |
| protected Element(PositionBounds bounds) { |
| this.bounds = bounds; |
| } |
| |
| |
| /** Getter for bounds property. */ |
| public PositionBounds getBounds() { |
| return bounds; |
| } |
| |
| /** |
| * Updates the element fields. This method is called after reparsing. |
| * @param elem the element to merge with |
| */ |
| void update(Element elem) { |
| this.bounds = elem.bounds; |
| } |
| |
| /** Fires property change event. |
| * @param name property name |
| * @param o old value |
| * @param n new value |
| */ |
| protected final void firePropertyChange(String name, Object o, Object n) { |
| support.firePropertyChange (name, o, n); |
| } |
| |
| /** Adds property listener */ |
| public void addPropertyChangeListener (PropertyChangeListener l) { |
| support.addPropertyChangeListener (l); |
| } |
| |
| /** Removes property listener */ |
| public void removePropertyChangeListener (PropertyChangeListener l) { |
| support.removePropertyChangeListener (l); |
| } |
| |
| /** Prints this element (and all its subelements) by calling <code>bounds.setText(...)</code> |
| * If <code>bounds</code> is null does nothing. |
| * @see #bounds */ |
| public final void print() { |
| if (bounds == null) { |
| return; |
| } |
| try { |
| bounds.setText(getDocumentString()); |
| } catch (BadLocationException e) { |
| ErrorManager.getDefault().notify(e); |
| } catch (IOException e) { |
| ErrorManager.getDefault().notify(e); |
| } |
| } |
| |
| /** |
| * Get a string representation of the element for printing into Document. |
| * It currently means that it's properly escaped. |
| * @return the string in its Document form |
| */ |
| public abstract String getDocumentString(); |
| |
| /** |
| * Get debug string of the element. |
| * @return the string |
| */ |
| public String toString() { |
| if (bounds == null) { |
| return "(no bounds)"; |
| } |
| return new StringBuffer(16) |
| .append('(') |
| .append(bounds.getBegin().getOffset()) |
| .append(", ") //NOI18N |
| .append(bounds.getEnd().getOffset()) |
| .append(')') |
| .toString(); |
| } |
| |
| |
| /** General class for basic elements, which contain value directly. */ |
| public static abstract class Basic extends Element { |
| |
| private static final String hexaDigitChars |
| = "0123456789abcdefABCDEF"; //NOI18N |
| |
| protected static void appendIsoControlChar(final StringBuilder buf, |
| final char c) { |
| switch (c) { |
| case '\t': |
| buf.append('\\').append('t'); |
| break; |
| case '\n': |
| buf.append('\\').append('n'); |
| break; |
| case '\f': |
| buf.append('\\').append('f'); |
| break; |
| case '\r': |
| buf.append('\\').append('r'); |
| break; |
| default: |
| buf.append('\\').append('u'); |
| for (int shift = 12; shift >= 0; shift -= 4) { |
| buf.append(hexaDigitChars.charAt( |
| ((c >> shift) & 0xf))); |
| } |
| } |
| } |
| |
| /** Parsed value of the element */ |
| protected String value; |
| |
| /** Create a new basic element. */ |
| protected Basic(PositionBounds bounds, String value) { |
| super(bounds); |
| this.value = value; |
| } |
| |
| /** |
| * Updates the element fields. This method is called after reparsing. |
| * @param elem elemnet to merge with |
| */ |
| void update(Element elem) { |
| super.update(elem); |
| this.value = ((Basic)elem).value; |
| } |
| |
| /** Get a string representation of the element. |
| * @return the string + bounds |
| */ |
| public String toString() { |
| return value + " " + super.toString(); // NOI18N |
| } |
| |
| /** |
| * Get a value of the element. |
| * @return the Java string (no escaping) |
| */ |
| public String getValue() { |
| return value; |
| } |
| |
| /** |
| * Sets the value. Does not check if the value has changed. |
| * The value is immediately propadated in text Document possibly |
| * triggering DocumentEvents. |
| * @param value Java string (no escaping) |
| */ |
| public void setValue(String value) { |
| this.value = value; |
| this.print(); |
| } |
| |
| @Override |
| public boolean equals(Object anObject) { |
| if(this == anObject) { |
| return true; |
| } |
| if(anObject instanceof Basic) { |
| Basic b = (Basic)anObject; |
| if(value == null) { |
| return b.value == null; |
| } |
| return value.equals(b.value); |
| } |
| return false; |
| } |
| |
| } // End of nested class Basic. |
| |
| |
| /** Class representing key element in properties file. */ |
| public static class KeyElem extends Basic { |
| |
| /** Generated serial version UID. */ |
| static final long serialVersionUID =6828294289485744331L; |
| |
| |
| /** Create a new key element. */ |
| protected KeyElem(PositionBounds bounds, String value) { |
| super(bounds, value); |
| } |
| |
| |
| /** Get a string representation of the key for printing. Treats the '=' sign as a part of the key |
| * @return the string |
| */ |
| public String getDocumentString() { |
| return escapeSpecialChars(value) + "="; //NOI18N |
| } |
| |
| /** |
| * |
| * @author Marian Petras |
| */ |
| private static final String escapeSpecialChars(final String text) { |
| StringBuilder buf = new StringBuilder(text.length() + 16); |
| |
| final int length = text.length(); |
| for (int i = 0; i < length; i++) { |
| char c = text.charAt(i); |
| if (c < 0x20) { |
| Basic.appendIsoControlChar(buf, c); |
| } else { |
| switch (c) { |
| case '#': |
| case '!': |
| if (i == 0) { |
| buf.append('\\'); |
| } |
| break; |
| case ' ': |
| case '=': |
| case ':': |
| case '\\': |
| buf.append('\\'); |
| break; |
| } |
| buf.append(c); |
| } |
| } |
| return buf.toString(); |
| } |
| |
| @Override |
| public boolean equals(Object anObject) { |
| return anObject instanceof KeyElem && super.equals(anObject); |
| } |
| |
| } // End of nested class KeyElem. |
| |
| |
| /** Class representing value element in properties files. */ |
| public static class ValueElem extends Basic { |
| |
| /** Generated serial version UID. */ |
| static final long serialVersionUID =4662649023463958853L; |
| |
| /** Create a new value element. */ |
| protected ValueElem(PositionBounds bounds, String value) { |
| super(bounds, value); |
| } |
| |
| /** Get a string representation of the value for printing. Appends end of the line after the value. |
| * @return the string |
| */ |
| public String getDocumentString() { |
| // escape outerspaces and continious line marks |
| return escapeSpecialChars(value) + "\n"; //NOI18N |
| } |
| |
| /** |
| * |
| * @author Marian Petras |
| */ |
| private static final String escapeSpecialChars(final String text) { |
| StringBuilder buf = new StringBuilder(text.length() + 16); |
| |
| boolean isInitialWhitespace = true; |
| final int length = text.length(); |
| for (int i = 0; i < length; i++) { |
| char c = text.charAt(i); |
| |
| boolean escape = false; |
| if (c == '\\') { |
| isInitialWhitespace = false; |
| escape = true; |
| } else if (isInitialWhitespace) { |
| if (c == ' ') { |
| escape = true; |
| } else { |
| isInitialWhitespace = (c == '\t') || (c == '\r') |
| || (c == '\n') || (c == '\f'); |
| } |
| } |
| |
| if (c < 0x20) { |
| Basic.appendIsoControlChar(buf, c); |
| } else { |
| if (escape) { |
| buf.append('\\'); |
| } |
| buf.append(c); |
| } |
| } |
| return buf.toString(); |
| } |
| |
| } // End of nested class ValueElem. |
| |
| /** |
| * Class representing comment element in properties files. <code>null</code> values of the |
| * string are legal and indicate that the comment is empty. It should contain |
| * pure comment string without comment markers. |
| */ |
| public static class CommentElem extends Basic { |
| |
| /** Genererated serial version UID. */ |
| static final long serialVersionUID =2418308580934815756L; |
| |
| |
| /** |
| * Create a new comment element. |
| * @param value Comment without its markers (leading '#' or '!'). Markers |
| * are automatically prepended while writing it down to Document. |
| */ |
| protected CommentElem(PositionBounds bounds, String value) { |
| super(bounds, value); |
| } |
| |
| |
| /** Get a string representation of the comment for printing. Makes sure every non-empty line starts with a # and |
| * that the last line is terminated with an end of line marker. |
| * @return the string |
| */ |
| public String getDocumentString() { |
| if (value == null || value.length() == 0) |
| return ""; // NOI18N |
| else { |
| // insert #s at the beginning of the lines which contain non-blank characters |
| // holds the last position where we might have to insert a # if this line contains non-blanks |
| StringBuffer sb = new StringBuffer(value); |
| // append the \n if missing |
| if (sb.charAt(sb.length() - 1) != '\n') { |
| sb.append('\n'); |
| } |
| int lineStart = 0; |
| boolean hasCommentChar = false; |
| for (int i=0; i<sb.length(); i++) { |
| char aChar = sb.charAt(i); |
| // new line |
| if (aChar == '\n') { |
| String line = sb.substring(lineStart, i); |
| String convertedLine = escapeSpecialChars(line); |
| sb.replace(lineStart, i, convertedLine); |
| |
| // shift the index: |
| i += convertedLine.length() - line.length(); |
| |
| // the next line starts after \n: |
| lineStart = i + 1; |
| |
| hasCommentChar = false; |
| } else if (!hasCommentChar |
| && UtilConvert.whiteSpaceChars.indexOf(aChar) == -1) { |
| // nonempty symbol |
| if ((aChar == '#') || (aChar == '!')) { |
| lineStart = i + 1; |
| } else { |
| // insert a # |
| sb.insert(lineStart, '#'); |
| i++; |
| lineStart = i; |
| } |
| hasCommentChar = true; |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * |
| * @author Marian Petras |
| */ |
| private static final String escapeSpecialChars(final String text) { |
| StringBuilder buf = new StringBuilder(text.length() + 16); |
| |
| final int length = text.length(); |
| for (int i = 0; i < length; i++) { |
| char c = text.charAt(i); |
| assert (c != '\n'); |
| |
| if ((c < 0x20) && (c != '\t')) { // keep tabs un-escaped in comments |
| Basic.appendIsoControlChar(buf, c); |
| continue; |
| } else if (c == '\\') { |
| buf.append('\\'); |
| } |
| buf.append(c); |
| } |
| return buf.toString(); |
| } |
| |
| @Override |
| public boolean equals(Object anObject) { |
| return anObject instanceof CommentElem && super.equals(anObject); |
| } |
| |
| } // End of nested CommentElem. |
| |
| |
| /** |
| * Class representing element in properties file. Each element contains comment (preceding the property), |
| * key and value subelement. |
| */ |
| public static class ItemElem extends Element implements Node.Cookie { |
| |
| /** Key element. */ |
| private KeyElem key; |
| |
| /** Value element. */ |
| private ValueElem value; |
| |
| /** Comment element. */ |
| private CommentElem comment; |
| |
| /** Parent of this element - active element has a non-null parent. */ |
| private PropertiesStructure parent; |
| |
| /** Name of the Key property */ |
| public static final String PROP_ITEM_KEY = "key"; // NOI18N |
| /** Name of the Value property */ |
| public static final String PROP_ITEM_VALUE = "value"; // NOI18N |
| /** Name of the Comment property */ |
| public static final String PROP_ITEM_COMMENT = "comment"; // NOI18N |
| |
| /** Generated serial version UID. */ |
| static final long serialVersionUID =1078147817847520586L; |
| |
| |
| /** Create a new basic element. <code>key</code> and <code>value</code> may be null. */ |
| protected ItemElem(PositionBounds bounds, KeyElem key, ValueElem value, CommentElem comment) { |
| super(bounds); |
| this.key = key; |
| this.value = value; |
| this.comment = comment; |
| } |
| |
| |
| /** Sets the parent of this element. */ |
| void setParent(PropertiesStructure ps) { |
| parent = ps; |
| } |
| |
| /** Gets parent. |
| * @exception IllegalStateException if the parent is <code>null</code>. */ |
| private PropertiesStructure getParent() { |
| if(parent == null) { |
| throw new IllegalStateException("Resource Bundle: Parent is missing"); // NOI18N |
| } |
| |
| return parent; |
| } |
| |
| /** Get a value string of the element. |
| * @return the string |
| */ |
| public String toString() { |
| return comment.toString() + "\n" + // NOI18N |
| ((key == null) ? "" : key.toString()) + "\n" + // NOI18N |
| ((value == null) ? "" : value.toString()) + "\n"; // NOI18N |
| } |
| |
| /** Returns the key element for this item. */ |
| public KeyElem getKeyElem() { |
| return key; |
| } |
| |
| /** Returns the value element for this item. */ |
| public ValueElem getValueElem() { |
| return value; |
| } |
| |
| /** Returns the comment element for this item. */ |
| public CommentElem getCommentElem() { |
| return comment; |
| } |
| |
| void update(Element elem) { |
| super.update(elem); |
| if (this.key == null) |
| this.key = ((ItemElem)elem).key; |
| else |
| this.key.update(((ItemElem)elem).key); |
| |
| if (this.value == null) |
| this.value = ((ItemElem)elem).value; |
| else |
| this.value.update(((ItemElem)elem).value); |
| |
| this.comment.update(((ItemElem)elem).comment); |
| } |
| |
| public String getDocumentString() { |
| return comment.getDocumentString() + |
| ((key == null) ? "" : key.getDocumentString()) + // NOI18N |
| ((value == null) ? "" : value.getDocumentString()); // NOI18N |
| } |
| |
| /** Get a key by which to identify this record |
| * @return nonescaped key |
| */ |
| public String getKey() { |
| return (key == null) ? null : key.getValue(); |
| } |
| |
| /** Set the key for this item |
| * @param newKey nonescaped key |
| */ |
| public void setKey(String newKey) { |
| String oldKey = key.getValue(); |
| if (!oldKey.equals(newKey)) { |
| key.setValue(newKey); |
| getParent().itemKeyChanged(oldKey, this); |
| this.firePropertyChange(PROP_ITEM_KEY, oldKey, newKey); |
| } |
| } |
| |
| /** Get the value of this item */ |
| public String getValue() { |
| return (value == null) ? null : value.getValue(); |
| } |
| |
| /** Set the value of this item |
| * @param newValue the new value |
| */ |
| public void setValue(String newValue) { |
| String oldValue = value.getValue(); |
| if (!oldValue.equals(newValue)) { |
| |
| if(oldValue.equals("")) // NOI18N |
| // Reprint key for the case it's alone yet and doesn't have seprator after (= : or whitespace). |
| key.print(); |
| |
| value.setValue(newValue); |
| getParent().itemChanged(this); |
| this.firePropertyChange(PROP_ITEM_VALUE, oldValue, newValue); |
| } |
| } |
| |
| /** Get the comment for this item */ |
| public String getComment() { |
| return (comment == null) ? null : comment.getValue(); |
| } |
| |
| /** Set the comment for this item |
| * @param newComment the new comment (escaped value) |
| * //??? why is required escaped value? I'd expect escapng to be applied during |
| * writing value down to stream no earlier |
| */ |
| public void setComment(String newComment) { |
| String oldComment = comment.getValue(); |
| if ((oldComment == null && newComment != null) || (oldComment != null && !oldComment.equals(newComment))) { |
| comment.setValue(newComment); |
| getParent().itemChanged(this); |
| this.firePropertyChange(PROP_ITEM_COMMENT, oldComment, newComment); |
| } |
| } |
| |
| /** Checks for equality of two ItemElem-s */ |
| public boolean equals(Object item) { |
| if (item == null || !(item instanceof ItemElem)) |
| return false; |
| ItemElem ie = (ItemElem)item; |
| return isKeyEqual(ie) && isValueEqual(ie) && isCommentEqual(ie); |
| } |
| |
| private boolean isKeyEqual(ItemElem ie) { |
| if(key==null) { |
| return ie.key==null; |
| } |
| return key.equals(ie.key); |
| } |
| |
| private boolean isValueEqual(ItemElem ie) { |
| if(value==null) { |
| return ie.value==null; |
| } |
| return value.equals(ie.value); |
| } |
| |
| private boolean isCommentEqual(ItemElem ie) { |
| if(comment==null) { |
| return ie.comment==null; |
| } |
| return comment.equals(ie.comment); |
| } |
| |
| } // End of nested class ItemElem. |
| } |