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