11/*
2- * Copyright IBM Corp. 2015, 2016
2+ * Copyright IBM Corp. 2015, 2017
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.
2222import java .io .OutputStream ;
2323import java .io .OutputStreamWriter ;
2424import java .nio .charset .StandardCharsets ;
25- import java .util .LinkedHashMap ;
25+ import java .text .StringCharacterIterator ;
26+ import java .util .ArrayList ;
27+ import java .util .Collections ;
28+ import java .util .List ;
2629import java .util .Map ;
2730import java .util .TreeSet ;
31+ import java .util .regex .Matcher ;
32+ import java .util .regex .Pattern ;
2833
2934import com .google .gson .GsonBuilder ;
35+ import com .google .gson .JsonArray ;
3036import com .google .gson .JsonElement ;
37+ import com .google .gson .JsonNull ;
38+ import com .google .gson .JsonObject ;
3139import com .google .gson .JsonParseException ;
3240import com .google .gson .JsonParser ;
41+ import com .google .gson .JsonPrimitive ;
42+ import com .google .gson .stream .JsonToken ;
3343import com .ibm .g11n .pipeline .resfilter .ResourceString .ResourceStringComparator ;
3444
3545/**
3646 * JSON resource filter implementation.
3747 *
38- * @author Yoshito Umaoka
48+ * @author Yoshito Umaoka, John Emmons
3949 */
4050public class JsonResource implements ResourceFilter {
4151
52+ private class KeyPiece {
53+ String keyValue ;
54+ JsonToken keyType ;
55+
56+ KeyPiece (String keyValue , JsonToken keyType ) {
57+ this .keyValue = keyValue ;
58+ this .keyType = keyType ;
59+ }
60+ }
61+
4262 @ Override
4363 public Bundle parse (InputStream inStream ) throws IOException {
4464 Bundle bundle = new Bundle ();
@@ -47,41 +67,167 @@ public Bundle parse(InputStream inStream) throws IOException {
4767 if (!root .isJsonObject ()) {
4868 throw new IllegalResourceFormatException ("The root JSON element is not an JSON object." );
4969 }
50- int sequenceNum = 0 ;
51- for (Map .Entry <String , JsonElement > entry : root .getAsJsonObject ().entrySet ()) {
52- String key = entry .getKey ();
53- JsonElement value = entry .getValue ();
54- if (!value .isJsonPrimitive () || !value .getAsJsonPrimitive ().isString ()) {
55- throw new IllegalResourceFormatException ("The value of JSON element " + key + " is not a string." );
56- }
57- sequenceNum ++;
58- bundle .addResourceString (key , value .getAsString (), sequenceNum );
59- }
70+ addBundleStrings (root .getAsJsonObject (), "" , bundle , 0 );
6071 } catch (JsonParseException e ) {
6172 throw new IllegalResourceFormatException ("Failed to parse the specified JSON contents." , e );
6273 }
6374 return bundle ;
6475 }
6576
77+ private int addBundleStrings (JsonObject obj , String keyPrefix , Bundle bundle , int sequenceNum ) {
78+ for (Map .Entry <String , JsonElement > entry : obj .entrySet ()) {
79+ String key = entry .getKey ();
80+ JsonElement value = entry .getValue ();
81+ if (value .isJsonObject ()) {
82+ sequenceNum = addBundleStrings (value .getAsJsonObject (), modifiedKeyPrefix (keyPrefix ,key ,"$." ) , bundle , sequenceNum );
83+ } else if (value .isJsonArray ()) {
84+ JsonArray ar = value .getAsJsonArray ();
85+ for (int i = 0 ; i < ar .size (); i ++) {
86+ JsonElement arrayEntry = ar .get (i );
87+ if (arrayEntry .isJsonPrimitive () && arrayEntry .getAsJsonPrimitive ().isString ()) {
88+ sequenceNum ++;
89+ bundle .addResourceString (modifiedKeyPrefix (keyPrefix ,key ,"$." ) + "[" + Integer .toString (i ) + "]" ,
90+ arrayEntry .getAsString (), sequenceNum );
91+
92+ } else {
93+ sequenceNum = addBundleStrings (arrayEntry .getAsJsonObject (),
94+ modifiedKeyPrefix (keyPrefix ,key ,"$." ) + "[" + Integer .toString (i ) + "]" , bundle , sequenceNum );
95+ }
96+ }
97+ } else if (!value .isJsonPrimitive () || !value .getAsJsonPrimitive ().isString ()) {
98+ throw new IllegalResourceFormatException ("The value of JSON element " + key + " is not a string." );
99+ } else {
100+ sequenceNum ++;
101+ bundle .addResourceString (modifiedKeyPrefix (keyPrefix ,key ,"" ), value .getAsString (), sequenceNum );
102+ }
103+ }
104+ return sequenceNum ;
105+ }
106+
107+ private String modifiedKeyPrefix ( String keyPrefix , String key , String addPrefixIfEmpty ) {
108+
109+ final Pattern specialSequences = Pattern .compile ("[.'\\ [\\ ]]" );
110+ if (key .isEmpty ()) {
111+ return keyPrefix ;
112+ }
113+ if (specialSequences .matcher (key ).find (0 )) {
114+ String modifiedKey = key .replaceAll ("'" , "\\ \\ u0027" );
115+ return keyPrefix + "['" + modifiedKey + "']" ;
116+ } else {
117+ if (keyPrefix .isEmpty ()) {
118+ return addPrefixIfEmpty + key ;
119+ }
120+ return keyPrefix + "." + key ;
121+ }
122+ }
123+
66124 @ Override
67125 public void write (OutputStream outStream , String language , Bundle bundle ) throws IOException {
68126 // extracts key value pairs in original sequence order
69127 TreeSet <ResourceString > sortedResources = new TreeSet <>(new ResourceStringComparator ());
70128 sortedResources .addAll (bundle .getResourceStrings ());
71- LinkedHashMap < String , String > kvmap = new LinkedHashMap <>( sortedResources . size () );
129+ JsonObject output = new JsonObject ( );
72130 for (ResourceString res : sortedResources ) {
73- kvmap .put (res .getKey (), res .getValue ());
131+ String key = res .getKey ();
132+ List <KeyPiece > keyPieces = splitKeyPieces (key );
133+ JsonElement current = output ;
134+ for (int i = 0 ; i < keyPieces .size (); i ++) {
135+ if (i + 1 < keyPieces .size ()) { // There is structure under this key piece
136+ if (current .isJsonObject ()) {
137+ JsonObject currentObject = current .getAsJsonObject ();
138+ if (!currentObject .has (keyPieces .get (i ).keyValue )) {
139+ if (keyPieces .get (i + 1 ).keyType == JsonToken .BEGIN_ARRAY ) {
140+ currentObject .add (keyPieces .get (i ).keyValue , new JsonArray ());
141+ } else {
142+ currentObject .add (keyPieces .get (i ).keyValue , new JsonObject ());
143+ }
144+ }
145+ current = currentObject .get (keyPieces .get (i ).keyValue );
146+ } else {
147+ JsonArray currentArray = current .getAsJsonArray ();
148+ Integer idx = Integer .valueOf (keyPieces .get (i ).keyValue );
149+ for ( int arrayIndex = currentArray .size (); arrayIndex <= idx ; arrayIndex ++) {
150+ currentArray .add (JsonNull .INSTANCE );
151+ }
152+ if (currentArray .get (idx ).isJsonNull ()) {
153+ if (keyPieces .get (i + 1 ).keyType == JsonToken .BEGIN_ARRAY ) {
154+ currentArray .set (idx , new JsonArray ());
155+ } else {
156+ currentArray .set (idx , new JsonObject ());
157+ }
158+ }
159+ current = currentArray .get (idx );
160+ }
161+ } else { // This is the leaf node
162+ if (keyPieces .get (i ).keyType == JsonToken .BEGIN_ARRAY ) {
163+ JsonArray currentArray = current .getAsJsonArray ();
164+ Integer idx = Integer .valueOf (keyPieces .get (i ).keyValue );
165+ JsonPrimitive e = new JsonPrimitive (res .getValue ());
166+ for ( int arrayIndex = currentArray .size (); arrayIndex <= idx ; arrayIndex ++) {
167+ currentArray .add (JsonNull .INSTANCE );
168+ }
169+ current .getAsJsonArray ().set (idx , e );
170+ } else {
171+ current .getAsJsonObject ().addProperty (keyPieces .get (i ).keyValue , res .getValue ());
172+ }
173+ }
174+ }
74175 }
75176 try (OutputStreamWriter writer = new OutputStreamWriter (new BufferedOutputStream (outStream ),
76177 StandardCharsets .UTF_8 )) {
77- new GsonBuilder ().setPrettyPrinting ().disableHtmlEscaping ().create ().toJson (kvmap , writer );
178+ new GsonBuilder ().setPrettyPrinting ().disableHtmlEscaping ().create ().toJson (output , writer );
179+ }
180+ }
181+
182+ private List <KeyPiece > splitKeyPieces (String key ) {
183+ List <KeyPiece > result = new ArrayList <KeyPiece >();
184+ Matcher onlyDigits = Pattern .compile ("^\\ d+$" ).matcher ("" );
185+ // Disregard $. at the beginning - it's not really part of the key...
186+ List <String > tokens = findTokens (key .startsWith ("$." ) ? key .substring (2 ) : key );
187+ for (String s : tokens ) {
188+ if (s .startsWith ("'" )) {
189+ // Turn any "\u0027" in the key back into '
190+ String modifiedKeyPiece = s .substring (1 , s .length () - 1 ).replaceAll ("\\ \\ u0027" , "'" );
191+ result .add (new KeyPiece (modifiedKeyPiece , JsonToken .BEGIN_OBJECT ));
192+ } else if (onlyDigits .reset (s ).matches ()) {
193+ result .add (new KeyPiece (s , JsonToken .BEGIN_ARRAY ));
194+ } else {
195+ for (String s2 : s .split ("\\ ." )) {
196+ if (!s2 .isEmpty ()) {
197+ result .add (new KeyPiece (s2 , JsonToken .BEGIN_OBJECT ));
198+ }
199+ }
200+ }
201+ }
202+ return Collections .unmodifiableList (result );
203+ }
204+
205+ private static List <String > findTokens (String data ) {
206+ List <String > tokens = new ArrayList <String >();
207+ boolean inQuotes = false ;
208+ StringBuilder currentToken = new StringBuilder ();
209+ StringCharacterIterator i = new StringCharacterIterator (data );
210+ while (i .current () != StringCharacterIterator .DONE ) {
211+ char c = i .current ();
212+ if ( c == '\'' ) {
213+ inQuotes = !inQuotes ;
214+ }
215+ if (!inQuotes && ( c == '.' || c == '[' || c == ']' )) {
216+ tokens .add (currentToken .toString ());
217+ currentToken .setLength (0 );
218+ } else {
219+ currentToken .append (c );
220+ }
221+ i .next ();
78222 }
223+ tokens .add (currentToken .toString ());
224+ return Collections .unmodifiableList (tokens );
79225 }
80226
81227 @ Override
82- public void merge (InputStream base , OutputStream outStream , String language , Bundle bundle )
83- throws IOException {
84- //TODO: Add merge implementation here. For now, fallback to write() operation.
228+ public void merge (InputStream base , OutputStream outStream , String language , Bundle bundle ) throws IOException {
229+ // TODO: Add merge implementation here. For now, fallback to write()
230+ // operation.
85231 write (outStream , language , bundle );
86232 }
87233}
0 commit comments