1313import java .util .regex .Pattern ;
1414import java .util .stream .Collectors ;
1515import java .util .stream .IntStream ;
16+ import java .util .stream .Stream ;
17+ import java .util .stream .StreamSupport ;
1618
1719/**
1820 * This class is a replacement for <code>java.util.Properties</code>, with the difference that it
@@ -36,6 +38,15 @@ public Properties(Properties defaults) {
3638 tokens = new ArrayList <>();
3739 }
3840
41+ private Properties (Properties defaults , List <PropertiesParser .Token > tokens ) {
42+ this .defaults = defaults ;
43+ values = new LinkedHashMap <>();
44+ this .tokens = tokens ;
45+ rawEntrySet ().forEach (e -> {
46+ values .put (unescape (e .getKey ()), unescape (e .getValue ()));
47+ });
48+ }
49+
3950 /**
4051 * Searches for the property with the specified key in this property list. If the key is not
4152 * found in this property list, the default property list, and its defaults, recursively, are
@@ -186,12 +197,24 @@ public void storeToXML(OutputStream os, String comment, String encoding) throws
186197 }
187198
188199 /**
189- * Returns the current properties table with all its defaults as a single flattened properties
190- * table
200+ * Returns the current properties table with all its defaults as a single
201+ * flattened properties table. NB: Result will have no formatting or comments!
191202 *
192203 * @return a <code>Properties</code> object
204+ * @deprecated Use <code>flattened()</code>
193205 */
206+ @ Deprecated
194207 public Properties flatten () {
208+ return flattened ();
209+ }
210+
211+ /**
212+ * Returns the current properties table with all its defaults as a single
213+ * flattened properties table. NB: Result will have no formatting or comments!
214+ *
215+ * @return a <code>Properties</code> object
216+ */
217+ public Properties flattened () {
195218 Properties result = new Properties ();
196219 flatten (result );
197220 return result ;
@@ -261,12 +284,25 @@ public Set<String> rawKeySet() {
261284 * @return a collection of raw values.
262285 */
263286 public Collection <String > rawValues () {
264- return IntStream . range ( 0 , tokens . size () )
265- .filter (idx -> tokens .get (idx ).type == PropertiesParser .Type .KEY )
266- .mapToObj ( idx -> tokens .get (idx + 2 ).getRaw ())
287+ return combined ( tokens )
288+ .filter (ts -> ts .get (0 ).type == PropertiesParser .Type .KEY )
289+ .map ( ts -> ts .get (2 ).getRaw ())
267290 .collect (Collectors .toList ());
268291 }
269292
293+ /**
294+ * Works like <code>entrySet()</code> but returning the raw values. Meaning that the values have
295+ * not been unescaped before being returned.
296+ *
297+ * @return A set of raw key-value entries
298+ */
299+ public Set <Entry <String , String >> rawEntrySet () {
300+ return combined (tokens )
301+ .filter (ts -> ts .get (0 ).type == PropertiesParser .Type .KEY )
302+ .map (ts -> new SimpleEntry <>(ts .get (0 ).getRaw (), ts .get (2 ).getRaw ()))
303+ .collect (Collectors .toCollection (LinkedHashSet ::new ));
304+ }
305+
270306 @ Override
271307 public String get (Object key ) {
272308 return values .get (key );
@@ -296,11 +332,11 @@ public String put(String key, String value) {
296332 if (key == null || value == null ) {
297333 throw new NullPointerException ();
298334 }
299- String rawValue = escape (value , false );
335+ String rawValue = escapeValue (value );
300336 if (values .containsKey (key )) {
301337 replaceValue (key , rawValue , value );
302338 } else {
303- String rawKey = escape (key , true );
339+ String rawKey = escapeKey (key );
304340 addNewKeyValue (rawKey , key , rawValue , value );
305341 }
306342 return values .put (key , value );
@@ -575,19 +611,48 @@ private Cursor indexOf(String key) {
575611 return index (
576612 tokens .indexOf (
577613 new PropertiesParser .Token (
578- PropertiesParser .Type .KEY , escape (key , true ), key )));
579- }
580-
581- private String escape (String raw , boolean forKey ) {
582- raw = raw .replace ("\\ " , "\\ \\ " );
583- raw = raw .replace ("\n " , "\\ n" );
584- raw = raw .replace ("\r " , "\\ r" );
585- raw = raw .replace ("\t " , "\\ t" );
586- raw = raw .replace ("\f " , "\\ f" );
587- if (forKey ) {
588- raw = raw .replace (" " , "\\ " );
614+ PropertiesParser .Type .KEY , escapeKey (key ), key )));
615+ }
616+
617+ private static String escapeValue (String value ) {
618+ return value
619+ .replace ("\\ " , "\\ \\ " )
620+ .replace ("\n " , "\\ n" )
621+ .replace ("\r " , "\\ r" )
622+ .replace ("\t " , "\\ t" )
623+ .replace ("\f " , "\\ f" );
624+ }
625+
626+ private static String escapeKey (String key ) {
627+ return escapeValue (key ).replace (" " , "\\ " );
628+ }
629+
630+ private static String escapeUnicode (String text ) {
631+ return replace (
632+ text ,
633+ "[^\\ x{0000}-\\ x{00FF}]" ,
634+ m -> "\\ \\ u" + String .format ("%04x" , (int )m .group (0 ).charAt (0 )));
635+ }
636+
637+ private static String unescapeUnicode (String escape ) {
638+ StringBuilder txt = new StringBuilder ();
639+ for (int i = 0 ; i < escape .length (); i ++) {
640+ char ch = escape .charAt (i );
641+ if (ch == '\\' ) {
642+ ch = escape .charAt (++i );
643+ if (ch == 'u' ) {
644+ String num = escape .substring (i + 1 , i + 5 );
645+ txt .append ((char ) Integer .parseInt (num , 16 ));
646+ i += 4 ;
647+ } else {
648+ txt .append ('\\' );
649+ txt .append (ch );
650+ }
651+ } else {
652+ txt .append (ch );
653+ }
589654 }
590- return raw ;
655+ return txt . toString () ;
591656 }
592657
593658 private static String replace (String input , String regex , Function <Matcher , String > callback ) {
@@ -605,6 +670,87 @@ private static String replace(String input, Pattern regex, Function<Matcher, Str
605670 return resultString .toString ();
606671 }
607672
673+ /**
674+ * Returns a copy of the object where all characters, in keys and values that are not in
675+ * the Unicode range of 0x0000-0x00FF, have been escaped. This is useful when using
676+ * <code>store()</code> to write to an output that does not support UTF8.
677+ *
678+ * @return A <code>Properties</code> with encoded keys and values
679+ */
680+ public Properties escaped () {
681+ return new Properties (defaults != null ? defaults .escaped () : null , escapeTokens (tokens ));
682+ }
683+
684+ private static List <PropertiesParser .Token > escapeTokens (List <PropertiesParser .Token > tokens ) {
685+ return mapKeyValues (tokens , ts -> Arrays .asList (escapeToken (ts .get (0 )), ts .get (1 ), escapeToken (ts .get (2 ))));
686+ }
687+
688+ private static PropertiesParser .Token escapeToken (PropertiesParser .Token token ) {
689+ String raw = escapeUnicode (token .raw );
690+ if (!raw .equals (token .raw )) {
691+ token = new PropertiesParser .Token (token .type , raw , token .text );
692+ }
693+ return token ;
694+ }
695+
696+ /**
697+ * Returns a copy of the object where all Unicode escape sequences, in keys and values,
698+ * have been decoded into their actual Unicode characters. This is useful when using
699+ * <code>store()</code> to write to an output that supports UTF8.
700+ *
701+ * @return A <code>Properties</code> without Unicode escape sequences in its keys and values
702+ */
703+ public Properties unescaped () {
704+ return new Properties (defaults != null ? defaults .unescaped () : null , unescapeTokens (tokens ));
705+ }
706+
707+ private static List <PropertiesParser .Token > unescapeTokens (List <PropertiesParser .Token > tokens ) {
708+ return mapKeyValues (tokens , ts -> Arrays .asList (unescapeToken (ts .get (0 )), ts .get (1 ), unescapeToken (ts .get (2 ))));
709+ }
710+
711+ private static PropertiesParser .Token unescapeToken (PropertiesParser .Token token ) {
712+ String raw = unescapeUnicode (token .raw );
713+ if (!raw .equals (token .raw )) {
714+ token = new PropertiesParser .Token (token .type , raw , token .text );
715+ }
716+ return token ;
717+ }
718+
719+ private static List <PropertiesParser .Token > mapKeyValues (
720+ List <PropertiesParser .Token > tokens ,
721+ Function <List <PropertiesParser .Token >, List <PropertiesParser .Token >> mapper ) {
722+ return combined (tokens ).map (ts -> {
723+ if (ts .get (0 ).type == PropertiesParser .Type .KEY ) {
724+ return mapper .apply (ts );
725+ } else {
726+ return ts ;
727+ }
728+ }).flatMap (Collection ::stream ).collect (Collectors .toList ());
729+ }
730+
731+ private static Stream <List <PropertiesParser .Token >> combined (List <PropertiesParser .Token > tokens ) {
732+ Iterator <List <PropertiesParser .Token >> iter = new Iterator <List <PropertiesParser .Token >>() {
733+ Iterator <PropertiesParser .Token > i = tokens .iterator ();
734+
735+ @ Override
736+ public boolean hasNext () {
737+ return i .hasNext ();
738+ }
739+
740+ @ Override
741+ public List <PropertiesParser .Token > next () {
742+ PropertiesParser .Token t = i .next ();
743+ if (t .type == PropertiesParser .Type .KEY ) {
744+ return Arrays .asList (t , i .next (), i .next ());
745+ } else {
746+ return Collections .singletonList (t );
747+ }
748+ }
749+ };
750+
751+ return StreamSupport .stream (Spliterators .spliterator (iter , tokens .size (), Spliterator .SORTED ), false );
752+ }
753+
608754 /**
609755 * Returns a <code>java.util.Properties</code> with the same contents as this object. The
610756 * information is a copy, changes to one Properties object will not affect the other.
0 commit comments