/*******************************************************************************
* Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Exadel, Inc. and Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.common.model.loaders.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.jboss.tools.common.model.*;
import org.jboss.tools.common.model.loaders.*;
import org.jboss.tools.common.model.util.XModelObjectLoaderUtil;
import org.jboss.tools.common.model.event.XModelTreeEvent;
import org.jboss.tools.common.model.impl.*;
import org.jboss.tools.common.meta.XAttribute;
import org.jboss.tools.common.util.FileUtil;
public class PropertiesLoader implements XObjectLoader {
public static String ENT_PROPERTY = "Property"; //$NON-NLS-1$
// static String INTERNAL_SEPARATOR = "\\"; //$NON-NLS-1$
static String defaultLineSeparator;
static {
defaultLineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
if(defaultLineSeparator == null) {
defaultLineSeparator = "\r\n"; //$NON-NLS-1$
}
}
public PropertiesLoader() {}
public static String getEncoding(XModelObject object) {
if(!object.isActive()) {
String encoding = object.get("_encoding_"); //$NON-NLS-1$
return encoding != null && encoding.length() > 0 ? encoding : "8859_1"; //$NON-NLS-1$
}
IFile f = getFile(object);
return f != null ? FileUtil.getEncoding(f) : null;
}
private static IFile getFile(XModelObject object) {
Object resource = object.getAdapter(IResource.class);
return (resource instanceof IFile) ? (IFile)resource : null;
}
/**
* We scan body of file as set of tokens
* - line without symbols breaking line;
* - \r
* - \n
* While scanning tokens, depending on what is already processed we put
* our engine into one of possible states:
* state = 0 - we are about to read a new property. That is the case
* in the beginning and after reading of some property is
* completed.
* state = 1 - we have read something while being in state 0, that
* does not contain property. We do not have a property
* object yet to bind that symbol. We have to keep that
* state until we get a token that contains a property.
* If stream ends before that, all the accumulated text
* will be stored as a special property of file object.
* state = 2 - we have read a line that contains a property and the
* line is not concluded with symbol of property
* continuation '\'. Now we have to wait for line breaking
* symbols to complete processing the current property.
* After that, state will be set to 0.
* state = 3 - we have read a line that contains a property and the
* line is concluded with symbol of property
* continuation '\'. We have to continue to read the
* current property.
* state = 4 - while in state 3, we have read line breaking symbols.
* We have to note that, because, if next token will be
* again line breaking symbols, that will mean that the
* current property is completed.
* These 4 states are the complete set to describe the process of
* splitting properties file into property objects.
*/
public void load(XModelObject object) {
String encoding = getEncoding(object);
object.setAttributeValue("encoding", encoding); //$NON-NLS-1$
String body = XModelObjectLoaderUtil.getTempBody(object);
EncodedProperties properties = new EncodedProperties();
properties.setEncoding(encoding);
Properties mapping = new Properties();
try {
ByteArrayInputStream s = new ByteArrayInputStream(body.getBytes(encoding));
properties.load(s);
Iterator it = properties.keySet().iterator();
while(it.hasNext()) {
String nm = it.next().toString();
String sn = EncodedProperties.saveConvert(nm, true); // convertName(nm);
mapping.put(sn, nm);
}
} catch (IOException e) {
//ignore
}
StringTokenizer st = new StringTokenizer(body, "\n\r", true); //$NON-NLS-1$
StringBuilder sb = new StringBuilder();
StringBuilder lineEnd = new StringBuilder();
int state = 0;
XModelObject c = null;
while(st.hasMoreTokens()) {
String s = st.nextToken();
if(s.equals("\r")) { //$NON-NLS-1$
if(lineEnd.toString().equals("\r")) { //$NON-NLS-1$
if(state == 0) {
state = 1;
} else if(state == 2) {
state = 0;
c.setAttributeValue("dirtyvalue", sb.toString()); //$NON-NLS-1$
c.setAttributeValue("line-end", lineEnd.toString()); //$NON-NLS-1$
sb.setLength(0);
lineEnd.setLength(0);
}
}
if(state != 2 && state != 4) sb.append(s); else lineEnd.append(s);
continue;
}
if(s.equals("\n")) { //$NON-NLS-1$
if(state != 2) {
if(state != 4) {
sb.append(s);
} else {
lineEnd.append(s);
state = 2;
}
if(state == 3) {
state = 4;
}
} else lineEnd.append(s);
if(state == 0) {
state = 1;
} else if(state == 2) {
state = 0;
c.setAttributeValue("dirtyvalue", sb.toString()); //$NON-NLS-1$
c.setAttributeValue("line-end", lineEnd.toString()); //$NON-NLS-1$
sb.setLength(0);
lineEnd.setLength(0);
}
continue;
}
lineEnd.setLength(0);
if(state == 3 || state == 4) {
if(!endsWithBackslash(s)) {
sb.append(s);
state = 2;
} else {
sb.append(s);
state = 3;
}
continue;
}
int i = getSeparatorIndex(s);
if(i < 0) {
sb.append(s);
state = 1;
continue;
}
String dirtyName = s.substring(0, i);
String name = dirtyName.trim();
String visualName = mapping.getProperty(name);
if(visualName == null || !properties.containsKey(visualName)) {
sb.append(s);
state = 1;
continue;
}
String comments = sb.toString();
sb.setLength(0);
String value = properties.getProperty(visualName);
Properties p = new Properties();
p.setProperty(XModelObjectConstants.ATTR_NAME, visualName);
p.setProperty("dirtyname", dirtyName); //$NON-NLS-1$
p.setProperty("value", value); //$NON-NLS-1$
p.setProperty("name-value-separator", i == s.length() ? " " : "" + s.charAt(i)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
p.setProperty("comments", comments); //$NON-NLS-1$
p.setProperty("separator", "#"); //obsolete //$NON-NLS-1$ //$NON-NLS-2$
p.setProperty("line-end", ""); //$NON-NLS-1$ //$NON-NLS-2$
c = object.getModel().createModelObject(ENT_PROPERTY, p);
XModelObject q = object.getChildByPath(c.getPathPart());
if(q != null) {
int k = 1;
while(true) {
c.set(XModelObjectImpl.DUPLICATE, "" + k);
if(object.getChildByPath(c.getPathPart()) == null) break;
k++;
}
q.set(XModelObjectImpl.DUPLICATE, "" + k);
q.setAttributeValue("value", convertDirtyValue(q.getAttributeValue("dirtyvalue")));
c.set(XModelObjectImpl.DUPLICATE, "");
}
object.addChild(c);
String dirtyvalue = (i < s.length()) ? s.substring(i + 1) : ""; //$NON-NLS-1$
if(endsWithBackslash(s)) {
state = 3;
sb.append(dirtyvalue);
} else {
state = 2;
sb.append(dirtyvalue);
}
}
if(state == 1 && sb.length() > 0) {
object.set("conclusion", sb.toString()); //$NON-NLS-1$
}
if(state == 2) {
c.setAttributeValue("dirtyvalue", sb.toString()); //$NON-NLS-1$
c.setAttributeValue("line-end", lineEnd.toString()); //$NON-NLS-1$
}
}
boolean endsWithBackslash(String s) {
boolean result = false;
for (int i = s.length() - 1; i >= 0; i--) {
if(s.charAt(i) != '\\') return result;
result = !result;
}
return result;
}
public boolean update(XModelObject object) throws XModelException {
String encoding = getEncoding(object);
XModelObject c = object.copy(0);
if(encoding != null) c.set("_encoding_", encoding); //$NON-NLS-1$
XModelObjectLoaderUtil.setTempBody(c, XModelObjectLoaderUtil.getTempBody(object));
load(c);
////EnginesLoader.merge(object, c, false);
merge(object, c);
object.setModified(false);
return true;
}
public boolean save(XModelObject object) {
if(!object.isModified()) return true;
XModelObjectLoaderUtil.setTempBody(object, generateBody(object));
object.setModified(true);
return true;
}
private void appendComments(StringBuffer sb, String comments, String commentSeparator, String lineSeparator) {
if(comments.length() == 0) return;
sb.append(comments);
}
public String getBody(XModelObject object) {
return generateBody(object);
}
private String generateBody(XModelObject object) {
String lineSeparator = defaultLineSeparator;
StringBuffer sb = new StringBuffer();
XModelObject[] cs = object.getChildren();
for (int i = 0; i < cs.length; i++) {
String ls = cs[i].get("line-end"); //$NON-NLS-1$
if(ls.length() > 0 && !ls.equals("\\r\\n")) { //$NON-NLS-1$
lineSeparator = ls;
break;
}
}
for (int i = 0; i < cs.length; i++) {
String name_value_separator = cs[i].getAttributeValue("name-value-separator"); //$NON-NLS-1$
if(name_value_separator == null || name_value_separator.length() != 1 || " \t=:".indexOf(name_value_separator) < 0) { //$NON-NLS-1$
name_value_separator = "="; //$NON-NLS-1$
}
appendComments(sb, cs[i].get("COMMENTS"), cs[i].get("SEPARATOR"), lineSeparator); //$NON-NLS-1$ //$NON-NLS-2$
if(XModelObjectConstants.NO.equals(cs[i].get("ENABLED"))) sb.append('#'); //$NON-NLS-1$
String dirtyname = cs[i].getAttributeValue("dirtyname"); //$NON-NLS-1$
String name = EncodedProperties.saveConvert(cs[i].get(XModelObjectConstants.XML_ATTR_NAME), true);
String value = cs[i].get("VALUE"); //$NON-NLS-1$
String dirtyvalue = cs[i].getAttributeValue("dirtyvalue"); //$NON-NLS-1$
if(value == null || dirtyvalue == null || !value.equals(trimLeft(dirtyvalue))) {
value = EncodedProperties.saveConvert(value, false); // convertValue(value);
}
String resolved = resolveValue(value, dirtyvalue);
//preserve one white space after separator
if(dirtyvalue != null && dirtyvalue.startsWith(" ") //$NON-NLS-1$
&& resolved != null && resolved.length() > 0 && !resolved.startsWith(" ") //$NON-NLS-1$
&& !name_value_separator.endsWith(" ")) { //$NON-NLS-1$
resolved = " " + resolved; //$NON-NLS-1$
}
if(dirtyname != null && name.equals(dirtyname.trim())) name = dirtyname;
//preserve one white space before separator
if(dirtyname != null && dirtyname.endsWith(" ") //$NON-NLS-1$
&& name != null && name.length() > 0 && !name.endsWith(" ") //$NON-NLS-1$
&& !name_value_separator.startsWith(" ")) { //$NON-NLS-1$
name = name + " "; //$NON-NLS-1$
}
sb.append(name);
if(!" ".equals(name_value_separator) || resolved.length() > 0) { //$NON-NLS-1$
sb.append(name_value_separator);
}
sb.append(resolved);
String ls = cs[i].get("line-end"); //$NON-NLS-1$
if(ls.length() > 0) {
if(ls.equals("\\r\\n")) ls = lineSeparator; //$NON-NLS-1$
sb.append(ls);
} else if(i < cs.length - 1) {
ls = lineSeparator;
sb.append(ls);
}
}
String conclusion = object.get("conclusion"); //$NON-NLS-1$
if(conclusion != null) sb.append(conclusion);
return sb.toString();
}
private String trimLeft(String s) {
int qq = 0;
while(qq < s.length() && s.charAt(qq) <= ' ') qq++;
return (qq > 0) ? s.substring(qq) : s;
}
String resolveValue(String value, String dirtyvalue) {
if(dirtyvalue == null) return value;
if(trimLeft(dirtyvalue).equals(value)) return dirtyvalue;
String cv = convertDirtyValue(value);
String cdv = convertDirtyValue(dirtyvalue);
if(cv != null && cv.equals(cdv)) {
return dirtyvalue;
}
List<PropertyValueToken> t1 = new PropertyValueParser(dirtyvalue).tokens;
List<PropertyValueToken> t2 = new PropertyValueParser(value).tokens;
int i1 = 0;
int i2 = 0;
while(true) {
while(i1 < t1.size() && t1.get(i1).value.length() == 0) i1++;
while(i2 < t2.size() && t2.get(i2).value.length() == 0) i2++;
if(i1 < t1.size() && i2 < t2.size() && t1.get(i1).value.equals(t2.get(i2).value)) {
i1++;
i2++;
} else break;
}
int i3 = t1.size() - 1, i4 = t2.size() - 1;
while(true) {
while(i3 >= 0 && t1.get(i3).value.length() == 0) i3--;
while(i4 >= 0 && t2.get(i4).value.length() == 0) i4--;
if(i3 >= 0 && i4 >= 0 && t1.get(i3).value.equals(t2.get(i4).value)) {
i3--;
i4--;
} else break;
}
List<PropertyValueToken> t = new ArrayList<PropertyValueToken>();
for (int i = 0; i < i1; i++) t.add(t1.get(i));
for (int i = i2; i <= i4; i++) t.add(t2.get(i));
if(i3 < i1) i3 = i1 - 1;
for (int i = i3 + 1; i < t1.size(); i++) t.add(t1.get(i));
StringBuilder result = new StringBuilder();
for (PropertyValueToken q: t) result.append(q.source);
return result.toString();// value;
}
String convertDirtyValue(String dirtyvalue) {
ByteArrayInputStream sr = new ByteArrayInputStream(("a=" + dirtyvalue + "\nb=v").getBytes());
Properties p = new Properties();
try {
p.load(sr);
} catch (IOException e) {
//ignore
} catch (IllegalArgumentException e1) {
return null;
}
return p.getProperty("a");
}
public void edit(XModelObject object, String body) {
XModelObject c = object.copy(0);
XModelObjectLoaderUtil.setTempBody(c, body);
load(c);
object.fireObjectChanged(XModelTreeEvent.BEFORE_MERGE);
try {
merge(object, c);
} finally {
object.fireObjectChanged(XModelTreeEvent.AFTER_MERGE);
}
}
private void merge(XModelObject o1, XModelObject o2) {
XModelObject[] c1 = o1.getChildren();
Map<String,XModelObject> m1 = new HashMap<String,XModelObject>();
for (int i = 0; i < c1.length; i++) m1.put(c1[i].getPathPart(), c1[i]);
XModelObject[] c2 = o2.getChildren();
RegularObjectImpl impl1 = (RegularObjectImpl)o1;
boolean ch = c2.length != c1.length;
boolean mod = false;
for (int i = 0; i < c2.length; i++) {
XModelObject c = (XModelObject)m1.remove(c2[i].getPathPart());
if(c == null) {
c = c2[i].copy();
((XModelObjectImpl)c).setParent_0(impl1);
} else if(!c.isEqual(c2[i])) {
mod = true;
XAttribute[] as = c.getModelEntity().getAttributes();
for (int j = 0; j < as.length; j++) {
if(!as[j].isCopyable()) continue;
String n = as[j].getName();
String v1 = c.getAttributeValue(n);
String v2 = c2[i].getAttributeValue(n);
if(v2 != null && !v2.equals(v1))
c.setAttributeValue(n, v2);
}
}
if(!ch && c1[i] != c) {
ch = true;
}
c2[i] = c;
}
c1 = (XModelObject[])m1.values().toArray(new XModelObject[0]);
// for (int i = 0; i < c1.length; i++) c1[i].removeFromParent();
if(ch || c1.length > 0) {
impl1.replaceChildren(c2);
((XModelImpl)o1.getModel()).fireStructureChanged(o1);
}
String conclusion1 = o1.get("conclusion"); //$NON-NLS-1$
if(conclusion1 == null) conclusion1 = ""; //$NON-NLS-1$
String conclusion2 = o2.get("conclusion"); //$NON-NLS-1$
if(conclusion2 == null) conclusion2 = ""; //$NON-NLS-1$
if(!conclusion1.equals(conclusion2)) {
o1.set("conclusion", conclusion2); //$NON-NLS-1$
mod = true;
}
if(ch || mod) o1.setModified(true);
}
public static int getSeparatorIndex(String s) {
String tr = s.trim();
if(tr.length() == 0 || tr.charAt(0) == '#' || tr.charAt(0) == '!') return -1;
boolean n = false;
boolean backslash = false;
int firstWhiteSpace = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(backslash && (c == ' ' || c == 't' || c == 'n')) {
// do nothing
} else if(Character.isWhitespace(c)) {
if(n) {
if(firstWhiteSpace < 0) firstWhiteSpace = i;
//return i;
}
continue;
} else if(c == '=' || c == ':') {
if(!backslash) return i;
} else {
if(n && firstWhiteSpace >= 0) return firstWhiteSpace;
n = true;
}
if(c == '\\') {
backslash = !backslash;
} else {
backslash = false;
}
}
return s.length();
}
}
class PropertyValueParser {
static int SKIP_SPACES = 0;
String value;
int offset;
List<PropertyValueToken> tokens;
public PropertyValueParser(String value) {
this.value = value;
parse();
}
public String getValue() {
StringBuilder sb = new StringBuilder();
for (PropertyValueToken t: tokens) {
sb.append(t.value);
}
return sb.toString();
}
public String getSource() {
StringBuilder sb = new StringBuilder();
for (PropertyValueToken t: tokens) {
sb.append(t.source);
}
return sb.toString();
}
void parse() {
tokens = new ArrayList<PropertyValueToken>();
skipSpaces();
boolean backSlash = false;
while(offset < value.length()) {
char ch = value.charAt(offset);
if(ch == '\\') {
offset++;
backSlash = !backSlash;
if(!backSlash) {
tokens.add(new PropertyValueToken(value.substring(offset - 2, offset), "\\"));
}
} else {
if(backSlash) {
backSlash = false;
if(ch == '\r' || ch == '\n') {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset), ""));
skipSpaces();
continue;
} else if(ch == 't') {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset + 1), "\t"));
offset++;
} else if(ch == 'f') {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset + 1), "\f"));
offset++;
} else if(ch == 'r') {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset + 1), "\r"));
offset++;
} else if(ch == 'n') {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset + 1), "\n"));
offset++;
} else if(ch == 'u') {
offset++;
int code = 0;
for (int i = offset; i < offset + 4; i++) {
char ch1 = value.charAt(i);
int q = 0;
if(ch1 >= '0' && ch1 <= '9') {
q = (int)(ch1 - '0');
} else if(ch1 >= 'a' && ch1 <= 'f') {
q = 10 + (int)(ch1 - 'a');
} else if(ch1 >= 'A' && ch1 <= 'F') {
q = 10 + (int)(ch1 - 'A');
}
code = code * 16 + q;
}
tokens.add(new PropertyValueToken(value.substring(offset - 2, offset + 4), "" + (char)code));
offset += 4;
} else {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset + 1), "" + ch));
offset++;
}
} else {
tokens.add(new PropertyValueToken(value.substring(offset, offset + 1), "" + ch));
offset++;
}
}
}
if(backSlash) {
tokens.add(new PropertyValueToken(value.substring(offset - 1, offset), ""));
}
}
void skipSpaces() {
int off = offset;
while(off < value.length() && Character.isWhitespace(value.charAt(off))) {
off++;
}
if(off > offset) {
tokens.add(new PropertyValueToken(value.substring(offset, off), ""));
offset = off;
}
}
}
class PropertyValueToken {
String source;
String value;
PropertyValueToken(String source, String value) {
this.source = source;
this.value = value;
}
}