View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.maven.plugins.i18n;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.nio.charset.StandardCharsets;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.Properties;
30  import java.util.SortedMap;
31  import java.util.TreeMap;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.lang3.Strings;
35  import org.jdom2.Document;
36  import org.jdom2.Element;
37  import org.jdom2.Namespace;
38  import org.jdom2.output.Format;
39  import org.jdom2.output.XMLOutputter;
40  
41  import jakarta.json.Json;
42  import jakarta.json.JsonObject;
43  import jakarta.json.JsonObjectBuilder;
44  
45  /**
46   * Helper class integrating i18n JSON generation into a sorted map.
47   */
48  class SlingI18nMap {
49  
50    private static final String JCR_LANGUAGE = "language";
51    private static final List<String> JCR_MIX_LANGUAGE = Collections.singletonList("mix:language");
52    private static final String JCR_MIXIN_TYPES = "mixinTypes";
53    private static final String JCR_NODETYPE_FOLDER = "nt:folder";
54    private static final String JCR_PRIMARY_TYPE = "primaryType";
55  
56    private static final String SLING_KEY = "key";
57    private static final String SLING_MESSAGE = "message";
58    private static final List<String> SLING_MESSAGE_MIXIN_TYPE = Collections.singletonList("sling:Message");
59  
60    private static final Namespace NAMESPACE_SLING = Namespace.getNamespace("sling", "http://sling.apache.org/jcr/sling/1.0");
61    private static final Namespace NAMESPACE_JCR = Namespace.getNamespace("jcr", "http://www.jcp.org/jcr/1.0");
62    private static final Namespace NAMESPACE_MIX = Namespace.getNamespace("mix", "http://www.jcp.org/jcr/mix/1.0");
63    private static final Namespace NAMESPACE_NT = Namespace.getNamespace("nt", "http://www.jcp.org/jcr/nt/1.0");
64  
65    private final String languageKey;
66    private final SortedMap<String, String> properties;
67  
68    /**
69     * @param languageKey Language key
70     */
71    SlingI18nMap(String languageKey, Map<String, String> properties) {
72      this.languageKey = languageKey;
73      this.properties = new TreeMap<>(properties);
74    }
75  
76    /**
77     * Build i18n resource JSON in Sling i18n Message format.
78     * @return JSON
79     */
80    public String getI18nJsonString() {
81      return JsonUtil.toString(buildI18nJson());
82    }
83  
84    /**
85     * Build i18n resource JSON in Sling i18n Message format.
86     * @return JSON
87     */
88    public String getI18nJsonPropertiesString() {
89      return JsonUtil.toString(buildI18nJsonProperties());
90    }
91  
92    private JsonObject buildI18nJson() {
93  
94      // get root
95      JsonObjectBuilder jsonDocument = getMixLanguageJsonDocument();
96  
97      // add entries
98      for (Entry<String, String> entry : properties.entrySet()) {
99        String key = entry.getKey();
100       String escapedKey = validName(key);
101       JsonObject value = getJsonI18nValue(key, entry.getValue(), !Strings.CS.equals(key, escapedKey));
102 
103       jsonDocument.add(escapedKey, value);
104     }
105 
106     // return result
107     return jsonDocument.build();
108   }
109 
110   private JsonObject buildI18nJsonProperties() {
111     JsonObjectBuilder jsonDocument = Json.createObjectBuilder();
112 
113     // add entries
114     for (Entry<String, String> entry : properties.entrySet()) {
115       String key = entry.getKey();
116       String escapedKey = validName(key);
117       jsonDocument.add(escapedKey, entry.getValue());
118     }
119 
120     // return result
121     return jsonDocument.build();
122   }
123 
124   private JsonObjectBuilder getMixLanguageJsonDocument() {
125     JsonObjectBuilder root = Json.createObjectBuilder();
126 
127     // add boiler plate
128     root.add("jcr:" + JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER);
129     root.add("jcr:" + JCR_MIXIN_TYPES, Json.createArrayBuilder(JCR_MIX_LANGUAGE).build());
130 
131     // add language
132     root.add("jcr:" + JCR_LANGUAGE, languageKey);
133 
134     return root;
135   }
136 
137   private JsonObject getJsonI18nValue(String key, String value, boolean generatedKeyProperty) {
138     JsonObjectBuilder valueNode = Json.createObjectBuilder();
139 
140     // add boiler plate
141     valueNode.add("jcr:" + JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER);
142     valueNode.add("jcr:" + JCR_MIXIN_TYPES, Json.createArrayBuilder(SLING_MESSAGE_MIXIN_TYPE).build());
143 
144     // add extra key attribute
145     if (generatedKeyProperty) {
146       valueNode.add("sling:" + SLING_KEY, key);
147     }
148 
149     // add actual i18n value
150     valueNode.add("sling:" + SLING_MESSAGE, value);
151 
152     return valueNode.build();
153   }
154 
155   /**
156    * Build i18n resource XML in Sling i18n Message format.
157    * @return XML
158    */
159   public String getI18nXmlString() {
160     Format format = Format.getPrettyFormat();
161     XMLOutputter outputter = new XMLOutputter(format);
162     return outputter.outputString(buildI18nXml());
163   }
164 
165   private Document buildI18nXml() {
166 
167     // get root
168     Document xmlDocument = getMixLanguageXmlDocument();
169 
170     // add entries
171     for (Entry<String, String> entry : properties.entrySet()) {
172       String key = entry.getKey();
173       String escapedKey = validName(key);
174       Element value = getXmlI18nValue(escapedKey, key, entry.getValue(), !Strings.CS.equals(key, escapedKey));
175 
176       xmlDocument.getRootElement().addContent(value);
177     }
178 
179     // return result
180     return xmlDocument;
181   }
182 
183   private Document getMixLanguageXmlDocument() {
184     Document doc = new Document();
185     Element root = new Element("root", NAMESPACE_JCR);
186     root.addNamespaceDeclaration(NAMESPACE_JCR);
187     root.addNamespaceDeclaration(NAMESPACE_MIX);
188     root.addNamespaceDeclaration(NAMESPACE_NT);
189     root.addNamespaceDeclaration(NAMESPACE_SLING);
190     doc.setRootElement(root);
191 
192     // add boiler plate
193     root.setAttribute(JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER, NAMESPACE_JCR);
194     root.setAttribute(JCR_MIXIN_TYPES, "[" + StringUtils.join(JCR_MIX_LANGUAGE, ",") + "]", NAMESPACE_JCR);
195 
196     // add language
197     root.setAttribute(JCR_LANGUAGE, languageKey, NAMESPACE_JCR);
198 
199     return doc;
200   }
201 
202   private Element getXmlI18nValue(String escapedKey, String key, String value, boolean generatedKeyProperty) {
203     Element valueNode = new Element(escapedKey);
204 
205     // add boiler plate
206     valueNode.setAttribute(JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER, NAMESPACE_JCR);
207     valueNode.setAttribute(JCR_MIXIN_TYPES, "[" + StringUtils.join(SLING_MESSAGE_MIXIN_TYPE, ",") + "]", NAMESPACE_JCR);
208 
209     // add extra key attribute
210     if (generatedKeyProperty) {
211       valueNode.setAttribute(SLING_KEY, key, NAMESPACE_SLING);
212     }
213 
214     // add actual i18n value
215     valueNode.setAttribute(SLING_MESSAGE, value, NAMESPACE_SLING);
216 
217     return valueNode;
218   }
219 
220   /**
221    * Creates a valid node name. Replaces all chars not in a-z, A-Z and 0-9 or '_', '.' with '-'.
222    * @param value String to be labelized.
223    * @return The labelized string.
224    */
225   private static String validName(String value) {
226 
227     // replace some special chars first
228     String text = value;
229     text = Strings.CS.replace(text, "ä", "ae");
230     text = Strings.CS.replace(text, "ö", "oe");
231     text = Strings.CS.replace(text, "ü", "ue");
232     text = Strings.CS.replace(text, "ß", "ss");
233 
234     // replace all invalid chars
235     StringBuilder sb = new StringBuilder(text);
236     for (int i = 0; i < sb.length(); i++) {
237       char ch = sb.charAt(i);
238       if (!((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
239           || (ch == '_') || (ch == '.'))) {
240         ch = '-';
241         sb.setCharAt(i, ch);
242       }
243     }
244     return sb.toString();
245   }
246 
247   /**
248    * Build i18n resource PROPERTIES.
249    * @return JSON
250    */
251   public String getI18nPropertiesString() throws IOException {
252     // Load all properties
253     Properties i18nProps = new Properties();
254 
255     // add entries
256     for (Entry<String, String> entry : properties.entrySet()) {
257       String key = entry.getKey();
258       String escapedKey = validName(key);
259       i18nProps.put(escapedKey, entry.getValue());
260     }
261 
262     try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
263       i18nProps.store(outStream, null);
264       // Property files are always ISO 8859 encoded
265       return outStream.toString(StandardCharsets.ISO_8859_1.name());
266     }
267   }
268 
269 }