/** * (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others. * * All rights reserved. This program and the accompanying materials * are 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 */ /** * */ package org.jabylon.properties.types.impl; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Iterator; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.ContentHandler.ByteOrderMark; import org.jabylon.properties.PropertiesFactory; import org.jabylon.properties.PropertiesPackage; import org.jabylon.properties.Property; import org.jabylon.properties.PropertyAnnotation; import org.jabylon.properties.PropertyFile; import org.jabylon.properties.types.PropertyAnnotations; import org.jabylon.properties.types.PropertyConverter; import org.jabylon.properties.util.NativeToAsciiConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author joe * */ public class PropertiesHelper implements PropertyConverter { private boolean unicodeEscaping; private static final int MAX_BOM_LENGTH = 4; /** the license header at the beginning of the file (if any)*/ private String licenseHeader; /** keeps track if we already checked for a license header*/ private boolean checkedForHeader; /** to be able to log the path */ private URI uri; /** current line number */ private long lineNo = 0; final Logger logger = LoggerFactory.getLogger(PropertiesHelper.class); public PropertiesHelper() { this(true); checkedForHeader = false; } public PropertiesHelper(boolean unicodeEscaping) { this(unicodeEscaping,null); } /** * * @param unicodeEscaping * @param uri can be used to log the path of invalid files if supplied */ public PropertiesHelper(boolean unicodeEscaping, URI uri) { this.unicodeEscaping = unicodeEscaping; if(uri==null) uri = URI.createURI("NONE SUPPLIED"); this.uri = uri; } public Property readProperty(BufferedReader reader) throws IOException { String line = null; Property property = null; StringBuilder comment = new StringBuilder(); StringBuilder propertyValue = new StringBuilder(); while((line = reader.readLine())!=null) { lineNo++; line=line.trim(); if(line.length()==0) { if(!checkedForHeader) { licenseHeader = comment.toString(); comment.setLength(0); checkedForHeader = true; } else { continue; } } if(isComment(line)) { if(comment.length()>0) //there's already a comment, so now we have a new line comment.append("\n"); if(line.length()>1) //otherwise it's just an empty comment comment.append(parseComment(line)); } else { propertyValue.append(NativeToAsciiConverter.convertEncodedToUnicode(line)); if(line.endsWith("\\")) { //if the line ends with a \ we need to continue reading in the next line continue; } property = PropertiesFactory.eINSTANCE.createProperty(); if(comment.length()>0) { property.setComment(comment.toString()); PropertyAnnotation nonTranslatable = property.findAnnotation(PropertyAnnotations.NON_TRANSLATABLE); if(nonTranslatable!=null) { logger.info("Property {} in file {} is marked as non-translatable. Skipping",property.getKey(),uri); propertyValue.setLength(0); comment.setLength(0);; continue; } } if(propertyValue.length()==0) continue; String[] parts = split(propertyValue.toString()); if(parts == null || parts[0]==null) //invalid property { logger.error("Invalid line {}: \"{}\" in property file \"{}\". Skipping", lineNo, propertyValue, uri); propertyValue.setLength(0); continue; } property.setKey(parts[0]); property.setValue(parts[1]); checkedForHeader = true; return property; } } //in some cases we already created an instance, but then never found a key //in that case, return null instead of an incomplete property //http://github.com/jutzig/jabylon/issues/issue/104 if(property!=null && property.getKey()==null) return null; return property; } /** * extracts the comment from the given line * @param line * @return the comment content */ protected String parseComment(String line) { return line.substring(1).trim(); } /** * splits the property line into a key and value part * @param propertyValue * @return */ protected String[] split(String propertyValue) { boolean escape = false; StringBuilder buffer = new StringBuilder(propertyValue.length()); String[] result = new String[2]; for (char c : propertyValue.toCharArray()) { if(!escape) { if(result[0]==null && (c==':' || c=='=')) { String string = buffer.toString().trim(); if(string.length()>0) result[0]=string; else return null; buffer.setLength(0); continue; } else if(c=='\\') { escape = true; continue; } } escape = false; buffer.append(c); } String string = buffer.toString(); if(string.startsWith(" ")) //remove trailing space '= value' string = string.substring(1); if(string.length()>0) result[1] = string; return result; } protected boolean isComment(String line) { return (line.startsWith("#") || line.startsWith("!")); } public void writeProperty(Writer writer, Property property) throws IOException { writeCommentAndAnnotations(writer, property); String key = property.getKey(); key = key.replaceAll("([ :=\n])", "\\\\$1"); if(unicodeEscaping) writer.write(NativeToAsciiConverter.convertUnicodeToEncoded(key,true)); else writer.write(key); writer.write(" = "); String value = property.getValue(); if(value!=null) { //leading spaces need to be masked //see https://github.com/jutzig/jabylon/issues/186 if(value.startsWith(" ")) value = "\\"+value; value = value.replace("\r", "\\r"); value = value.replace("\n", "\\n"); if(unicodeEscaping) value = NativeToAsciiConverter.convertUnicodeToEncoded(value, true); writer.write(value); } writer.write('\n'); } protected void writeCommentAndAnnotations(Writer writer, Property property) throws IOException { if(property.eIsSet(PropertiesPackage.Literals.PROPERTY__COMMENT) || property.getAnnotations().size()>0) { StringBuilder builder = new StringBuilder(); for (PropertyAnnotation annotation : property.getAnnotations()) { builder.append(annotation); } if(builder.length()>0) { builder.append("\n"); } builder.append(property.getCommentWithoutAnnotations()); writeComment(writer,builder.toString()); } } protected void writeComment(Writer writer, String comment) throws IOException { comment = comment.replace("\n", "\n#"); writer.write("#"); writer.write(comment); writer.write('\n'); } /** * returns the BOM if available. If no BOM was found the stream is reset to its original state * * @param inputStream must support mark/rest, or an IllegalArgumentException is thrown * @return * @throws IOException, IllegalArgumentException */ public static ByteOrderMark checkForBom(InputStream inputStream) throws IOException { if(!inputStream.markSupported()) throw new IllegalArgumentException("InputStream must support mark/rest: "+inputStream); inputStream.mark(MAX_BOM_LENGTH); ByteOrderMark bom = ByteOrderMark.read(inputStream); if(bom==null) inputStream.reset(); return bom; } public String getLicenseHeader() { return licenseHeader; } public void writeLicenseHeader(Writer writer, String licenseHeader) throws IOException { if(licenseHeader==null || licenseHeader.isEmpty()) return; writeComment(writer, licenseHeader); writer.write('\n'); } @Override public int write(OutputStream out, PropertyFile file, String encoding) throws IOException { int savedProperties = 0; BufferedWriter writer; if("UTF-8".equals(encoding)) { //see https://github.com/jutzig/jabylon/issues/5 //write BOMs in unicode mode out.write(ByteOrderMark.UTF_8.bytes()); } writer = new BufferedWriter(new OutputStreamWriter(out, encoding)); try { writeLicenseHeader(writer, file.getLicenseHeader()); Iterator<Property> it = file.getProperties().iterator(); while (it.hasNext()) { Property property = (Property) it.next(); //eliminate all empty property entries if(isFilled(property)) { writeProperty(writer, property); savedProperties++; } } } finally{ writer.close(); } return savedProperties; } protected boolean isFilled(Property property) { if(property==null) return false; if(property.getKey()==null || property.getKey().length()==0) return false; return !(property.getValue()==null || property.getValue().length()==0); } @Override public PropertyFile load(InputStream in, String encoding) throws IOException { if(!in.markSupported()) in = new BufferedInputStream(in); ByteOrderMark bom = PropertiesHelper.checkForBom(in); String derivedEncoding = deriveEncoding(bom); if(derivedEncoding!=null){ if(encoding.equals("UTF-16") && derivedEncoding.startsWith("UTF-16")) //the derived encoding will know if it's BE or LE encoding = derivedEncoding; else if(!encoding.equals(derivedEncoding)){ logger.warn("Encoding was specified as {} but according to the BOM it is {}. Using the value from the BOM instead",encoding,derivedEncoding); encoding = derivedEncoding; } } BufferedReader reader = new BufferedReader(new InputStreamReader(in,encoding)); PropertyFile file = PropertiesFactory.eINSTANCE.createPropertyFile(); try { Property p = null; while((p = readProperty(reader))!=null) { file.getProperties().add(p); } } finally{ if(reader!=null) { reader.close(); } } file.setLicenseHeader(getLicenseHeader()); return file; } protected String deriveEncoding(ByteOrderMark bom) { if(bom==null) return null; switch (bom) { case UTF_16BE: return "UTF-16BE"; case UTF_16LE: return "UTF-16LE"; case UTF_8: return "UTF-8"; } return null; } public boolean isUnicodeEscaping() { return unicodeEscaping; } }