11/*
2- * Copyright IBM Corp. 2015
2+ * Copyright IBM Corp. 2015, 2016
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
4040
4141import com .ibm .g11n .pipeline .resfilter .ResourceString .ResourceStringComparator ;
4242
43+ import org .apache .commons .lang3 .StringEscapeUtils ;
44+
4345/**
4446 * Java properties resource filter implementation.
4547 *
@@ -49,7 +51,8 @@ public class JavaPropertiesResource implements ResourceFilter {
4951
5052 // TODO:
5153 // This is not a good idea. This implementation might work,
52- // but it depends on an assumption that java.util.Properties#load(InputStream)
54+ // but it depends on an assumption that
55+ // java.util.Properties#load(InputStream)
5356 // calls Properties#put(Object, Object).
5457
5558 @ SuppressWarnings ("serial" )
@@ -65,7 +68,7 @@ public Iterable<Object> orderedKeys() {
6568
6669 @ Override
6770 public Enumeration <Object > keys () {
68- return Collections .<Object >enumeration (keys );
71+ return Collections .<Object > enumeration (keys );
6972 }
7073
7174 @ Override
@@ -78,17 +81,85 @@ public Object put(Object key, Object value) {
7881 @ Override
7982 public Bundle parse (InputStream inStream ) throws IOException {
8083 LinkedProperties props = new LinkedProperties ();
81- props .load (inStream );
84+ BufferedReader inStreamReader = new BufferedReader (new InputStreamReader (inStream , PROPS_ENC ));
85+ String line ;
86+ Map <String , List <String >> notesMap = new HashMap <>();
87+ List <String > currentNotes = new ArrayList <>();
88+ boolean globalNotesAvailable = true ;
89+ List <String > globalNotes = null ;
90+ while ((line = inStreamReader .readLine ()) != null ) {
91+ line = line .trim ();
92+ // Comment line - Add to list of comments (notes) until we find
93+ // either
94+ // a blank line (global comment) or a key/value pair
95+ if (line .startsWith ("#" ) || line .startsWith ("!" )) {
96+ // Strip off the leading comment marker, and perform any
97+ // necessary unescaping here.
98+ currentNotes .add (StringEscapeUtils .unescapeJava (line .substring (1 )));
99+ } else if (line .isEmpty ()) {
100+ // We are following the convention that the first blank line in
101+ // a properties
102+ // file signifies the end of a global comment.
103+ if (globalNotesAvailable && !currentNotes .isEmpty ()) {
104+ globalNotes = new ArrayList <>(currentNotes );
105+ currentNotes .clear ();
106+ } else {
107+ // Just a generic blank line - treat it like a comment.
108+ currentNotes .add (line );
109+ }
110+ globalNotesAvailable = false ;
111+ } else {
112+ // Regular non-comment line. If there are notes outstanding that
113+ // apply
114+ // to this line, we find its key and add it to the notes map.
115+ StringBuffer sb = new StringBuffer (line );
116+ while (isContinuationLine (sb .toString ())) {
117+ String continuationLine = inStreamReader .readLine ();
118+ sb .setLength (sb .length () - 1 ); // Remove the continuation
119+ // "\"
120+ if (continuationLine != null ) {
121+ sb .append (continuationLine .trim ());
122+ }
123+ }
124+ String logicalLine = sb .toString ();
125+ PropDef pd = PropDef .parseLine (logicalLine );
126+ props .setProperty (pd .getKey (), pd .getValue ());
127+ if (!currentNotes .isEmpty ()) {
128+ notesMap .put (pd .getKey (), new ArrayList <>(currentNotes ));
129+ currentNotes .clear ();
130+ }
131+ }
132+ }
133+
82134 Iterator <Object > i = props .orderedKeys ().iterator ();
83135 Bundle result = new Bundle ();
84136 int sequenceNum = 0 ;
85137 while (i .hasNext ()) {
86138 String key = (String ) i .next ();
87- result .addResourceString (key , props .getProperty (key ), ++sequenceNum );
139+ List <String > notes = notesMap .get (key );
140+ ResourceString rs = new ResourceString (key , props .getProperty (key ), ++sequenceNum , notes );
141+ result .addResourceString (rs );
142+ }
143+ if (globalNotes != null ) {
144+ result .addNotes (globalNotes );
88145 }
89146 return result ;
90147 }
91148
149+ // This method handles the bizarre edge case where someone might have
150+ // multiple backslashes at the end of a line. An even number of them
151+ // isn't really a continuation, but a backslash in the property value.
152+ private boolean isContinuationLine (String s ) {
153+ int backslashCount = 0 ;
154+ for (int index = s .length () - 1 ; index >= 0 ; index --) {
155+ if (s .charAt (index ) != '\\' ) {
156+ break ;
157+ }
158+ backslashCount ++;
159+ }
160+ return backslashCount % 2 == 1 ;
161+ }
162+
92163 @ Override
93164 public void write (OutputStream outStream , String language , Bundle resource ) throws IOException {
94165 TreeSet <ResourceString > sortedResources = new TreeSet <>(new ResourceStringComparator ());
@@ -109,9 +180,7 @@ static class PropDef {
109180 private PropSeparator separator ;
110181
111182 public enum PropSeparator {
112- EQUAL ('=' ),
113- COLON (':' ),
114- SPACE (' ' );
183+ EQUAL ('=' ), COLON (':' ), SPACE (' ' );
115184
116185 private char sepChar ;
117186
@@ -129,7 +198,7 @@ public char getCharacter() {
129198
130199 public PropDef (String key , String value , PropSeparator separator ) {
131200 this .key = key ;
132- this .value = value ; // This is raw value in file, and may contain escaped Unicode (e.g. \u00C1)
201+ this .value = value ;
133202 this .separator = separator ;
134203 };
135204
@@ -150,7 +219,7 @@ public static PropDef parseLine(String line) {
150219 sep = PropSeparator .SPACE ;
151220 }
152221 } else {
153- if (i > 0 && line .charAt (i - 1 ) != '\\' ) {
222+ if (i > 0 && line .charAt (i - 1 ) != '\\' ) {
154223 if (iChar == ' ' ) {
155224 sawSpace = true ;
156225 } else if (iChar == PropSeparator .EQUAL .getCharacter ()) {
@@ -171,8 +240,10 @@ public static PropDef parseLine(String line) {
171240 return null ;
172241 }
173242
174- PropDef pl = new PropDef (line .substring (0 , sepIdx ).trim (),
175- line .substring (sepIdx + 1 ).trim (), sep );
243+ String key = unescapePropKey (line .substring (0 , sepIdx ).trim ());
244+ String value = unescapePropValue (line .substring (sepIdx + 1 ).trim ());
245+
246+ PropDef pl = new PropDef (key , value , sep );
176247 return pl ;
177248 }
178249
@@ -190,14 +261,15 @@ public PropSeparator getSeparator() {
190261
191262 public void print (PrintWriter pw , String language ) throws IOException {
192263 StringBuilder buf = new StringBuilder (100 );
193- int len = key .length () + value .length () + 3 ; /* 3 - length of separator plus two SPs */
264+ int len = key .length () + value .length ()
265+ + 3 ; /* 3 - length of separator plus two SPs */
194266
195267 if (len <= COLMAX ) {
196268 // Print this property in a single line
197269 if (separator .getCharacter () == PropSeparator .SPACE .getCharacter ()) {
198- buf .append (key ).append (separator .getCharacter ());
270+ buf .append (escapePropKey ( key ) ).append (separator .getCharacter ());
199271 } else {
200- buf .append (key ).append (' ' ).append (separator .getCharacter ()).append (' ' );
272+ buf .append (escapePropKey ( key ) ).append (' ' ).append (separator .getCharacter ()).append (' ' );
201273 }
202274 buf .append (escapePropValue (value ));
203275 pw .println (buf .toString ());
@@ -208,9 +280,9 @@ public void print(PrintWriter pw, String language) throws IOException {
208280
209281 // always prints out key and separator in a single line
210282 if (separator .getCharacter () == PropSeparator .SPACE .getCharacter ()) {
211- buf .append (key ).append (separator .getCharacter ());
283+ buf .append (escapePropKey ( key ) ).append (separator .getCharacter ());
212284 } else {
213- buf .append (key ).append (' ' ).append (separator .getCharacter ()).append (' ' );
285+ buf .append (escapePropKey ( key ) ).append (' ' ).append (separator .getCharacter ()).append (' ' );
214286 }
215287
216288 if (buf .length () > COLMAX ) {
@@ -264,7 +336,8 @@ public void print(PrintWriter pw, String language) throws IOException {
264336
265337 @ Override
266338 public boolean equals (Object obj ) {
267- if (obj .getClass () != PropDef .class ) return false ;
339+ if (obj .getClass () != PropDef .class )
340+ return false ;
268341 PropDef p = (PropDef ) obj ;
269342 return getKey ().equals (p .getKey ()) && getValue ().equals (p .getValue ())
270343 && getSeparator ().getCharacter () == p .getSeparator ().getCharacter ();
@@ -290,8 +363,7 @@ private static String escapePropValue(String s) {
290363 if (c == '\\' ) {
291364 escaped .append ("\\ \\ " );
292365 } else if (c > 0x7F ) {
293- escaped .append ("\\ u" )
294- .append (String .format ("%04X" , (int )c ));
366+ escaped .append ("\\ u" ).append (String .format ("%04X" , (int ) c ));
295367 } else if (c == ':' ) {
296368 escaped .append ("\\ :" );
297369 } else if (c == '=' ) {
@@ -303,23 +375,54 @@ private static String escapePropValue(String s) {
303375 return escaped .toString ();
304376 }
305377
378+ private static String unescapePropValue (String s ) {
379+ StringBuilder unescaped = new StringBuilder ();
380+ StringCharacterIterator itr = new StringCharacterIterator (s );
381+ for (char c = itr .first (); c != CharacterIterator .DONE ; c = itr .next ()) {
382+ if (c == '\\' && itr .getIndex () < itr .getEndIndex ()) {
383+ char n = itr .next ();
384+ if (n == '\\' || n == ':' || n == '=' ) {
385+ unescaped .append (n );
386+ } else if (n == 'u' && itr .getIndex () + 4 <= itr .getEndIndex ()) {
387+ StringBuilder unicodeEscape = new StringBuilder ("\\ u" );
388+ for (int i = 0 ; i < 4 ; i ++) {
389+ unicodeEscape .append (itr .next ());
390+ }
391+ unescaped .append (StringEscapeUtils .unescapeJava (unicodeEscape .toString ()));
392+ } else {
393+ unescaped .append (c );
394+ unescaped .append (n );
395+ }
396+ } else {
397+ unescaped .append (c );
398+ }
399+ }
400+ return unescaped .toString ();
401+ }
402+
306403 private static String escapePropKey (String s ) {
307404 return s .replace (" " , "\\ " );
308405 }
309406
407+ private static String unescapePropKey (String s ) {
408+ return s .replaceAll ("\\ \\ " , " " );
409+ }
410+
310411 @ Override
311412 public void merge (InputStream base , OutputStream outStream , String language , Bundle resource ) throws IOException {
312- Map <String , String > resMap = new HashMap <String , String >(resource .getResourceStrings ().size () * 4 / 3 + 1 );
413+ Map <String , String > resMap = new HashMap <String , String >(resource .getResourceStrings ().size () * 4 / 3 + 1 );
313414 for (ResourceString res : resource .getResourceStrings ()) {
314- resMap .put (escapePropKey ( res .getKey () ), res .getValue ());
415+ resMap .put (res .getKey (), res .getValue ());
315416 }
316417
317418 BufferedReader baseReader = new BufferedReader (new InputStreamReader (base , PROPS_ENC ));
318419 PrintWriter outWriter = new PrintWriter (new OutputStreamWriter (outStream , PROPS_ENC ));
319420
320421 String line = null ;
321422 StringBuilder logicalLineBuf = new StringBuilder ();
322- List <String > orgLines = new ArrayList <String >(8 ); // default size - up to 8 continuous lines
423+ List <String > orgLines = new ArrayList <String >(8 ); // default size - up
424+ // to 8 continuous
425+ // lines
323426 do {
324427 // logical line that may define a single property, or empty line
325428 String logicalLine = null ;
@@ -339,7 +442,7 @@ public void merge(InputStream base, OutputStream outStream, String language, Bun
339442 if (normLine .startsWith ("#" ) || normLine .startsWith ("!" )) {
340443 // Comment line - print the original line
341444 outWriter .println (line );
342- } else if (normLine . endsWith ( " \\ " )) {
445+ } else if (isContinuationLine ( normLine )) {
343446 // Continue to the next line
344447 logicalLineBuf .append (normLine , 0 , normLine .length () - 1 );
345448 orgLines .add (line );
@@ -351,7 +454,7 @@ public void merge(InputStream base, OutputStream outStream, String language, Bun
351454 if (normLine .endsWith ("\\ " )) {
352455 // continues to the next line
353456 logicalLineBuf .append (normLine .substring (0 , normLine .length () - 1 ));
354- orgLines .add (line ); // preserve the original line
457+ orgLines .add (line ); // preserve the original line
355458 } else {
356459 // terminating the current logical property line
357460 logicalLineBuf .append (normLine );
0 commit comments