SlingI18nMap.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.wcm.maven.plugins.i18n;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.lang3.StringUtils;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;

/**
 * Helper class integrating i18n JSON generation into a sorted map.
 */
class SlingI18nMap {

  private static final String JCR_LANGUAGE = "language";
  private static final List<String> JCR_MIX_LANGUAGE = Collections.singletonList("mix:language");
  private static final String JCR_MIXIN_TYPES = "mixinTypes";
  private static final String JCR_NODETYPE_FOLDER = "nt:folder";
  private static final String JCR_PRIMARY_TYPE = "primaryType";

  private static final String SLING_KEY = "key";
  private static final String SLING_MESSAGE = "message";
  private static final List<String> SLING_MESSAGE_MIXIN_TYPE = Collections.singletonList("sling:Message");

  private static final Namespace NAMESPACE_SLING = Namespace.getNamespace("sling", "http://sling.apache.org/jcr/sling/1.0");
  private static final Namespace NAMESPACE_JCR = Namespace.getNamespace("jcr", "http://www.jcp.org/jcr/1.0");
  private static final Namespace NAMESPACE_MIX = Namespace.getNamespace("mix", "http://www.jcp.org/jcr/mix/1.0");
  private static final Namespace NAMESPACE_NT = Namespace.getNamespace("nt", "http://www.jcp.org/jcr/nt/1.0");

  private final String languageKey;
  private final SortedMap<String, String> properties;

  /**
   * @param languageKey Language key
   */
  SlingI18nMap(String languageKey, Map<String, String> properties) {
    this.languageKey = languageKey;
    this.properties = new TreeMap<>(properties);
  }

  /**
   * Build i18n resource JSON in Sling i18n Message format.
   * @return JSON
   */
  public String getI18nJsonString() {
    return JsonUtil.toString(buildI18nJson());
  }

  /**
   * Build i18n resource JSON in Sling i18n Message format.
   * @return JSON
   */
  public String getI18nJsonPropertiesString() {
    return JsonUtil.toString(buildI18nJsonProperties());
  }

  private JsonObject buildI18nJson() {

    // get root
    JsonObjectBuilder jsonDocument = getMixLanguageJsonDocument();

    // add entries
    for (Entry<String, String> entry : properties.entrySet()) {
      String key = entry.getKey();
      String escapedKey = validName(key);
      JsonObject value = getJsonI18nValue(key, entry.getValue(), !StringUtils.equals(key, escapedKey));

      jsonDocument.add(escapedKey, value);
    }

    // return result
    return jsonDocument.build();
  }

  private JsonObject buildI18nJsonProperties() {
    JsonObjectBuilder jsonDocument = Json.createObjectBuilder();

    // add entries
    for (Entry<String, String> entry : properties.entrySet()) {
      String key = entry.getKey();
      String escapedKey = validName(key);
      jsonDocument.add(escapedKey, entry.getValue());
    }

    // return result
    return jsonDocument.build();
  }

  private JsonObjectBuilder getMixLanguageJsonDocument() {
    JsonObjectBuilder root = Json.createObjectBuilder();

    // add boiler plate
    root.add("jcr:" + JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER);
    root.add("jcr:" + JCR_MIXIN_TYPES, Json.createArrayBuilder(JCR_MIX_LANGUAGE).build());

    // add language
    root.add("jcr:" + JCR_LANGUAGE, languageKey);

    return root;
  }

  private JsonObject getJsonI18nValue(String key, String value, boolean generatedKeyProperty) {
    JsonObjectBuilder valueNode = Json.createObjectBuilder();

    // add boiler plate
    valueNode.add("jcr:" + JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER);
    valueNode.add("jcr:" + JCR_MIXIN_TYPES, Json.createArrayBuilder(SLING_MESSAGE_MIXIN_TYPE).build());

    // add extra key attribute
    if (generatedKeyProperty) {
      valueNode.add("sling:" + SLING_KEY, key);
    }

    // add actual i18n value
    valueNode.add("sling:" + SLING_MESSAGE, value);

    return valueNode.build();
  }

  /**
   * Build i18n resource XML in Sling i18n Message format.
   * @return XML
   */
  public String getI18nXmlString() {
    Format format = Format.getPrettyFormat();
    XMLOutputter outputter = new XMLOutputter(format);
    return outputter.outputString(buildI18nXml());
  }

  private Document buildI18nXml() {

    // get root
    Document xmlDocument = getMixLanguageXmlDocument();

    // add entries
    for (Entry<String, String> entry : properties.entrySet()) {
      String key = entry.getKey();
      String escapedKey = validName(key);
      Element value = getXmlI18nValue(escapedKey, key, entry.getValue(), !StringUtils.equals(key, escapedKey));

      xmlDocument.getRootElement().addContent(value);
    }

    // return result
    return xmlDocument;
  }

  private Document getMixLanguageXmlDocument() {
    Document doc = new Document();
    Element root = new Element("root", NAMESPACE_JCR);
    root.addNamespaceDeclaration(NAMESPACE_JCR);
    root.addNamespaceDeclaration(NAMESPACE_MIX);
    root.addNamespaceDeclaration(NAMESPACE_NT);
    root.addNamespaceDeclaration(NAMESPACE_SLING);
    doc.setRootElement(root);

    // add boiler plate
    root.setAttribute(JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER, NAMESPACE_JCR);
    root.setAttribute(JCR_MIXIN_TYPES, "[" + StringUtils.join(JCR_MIX_LANGUAGE, ",") + "]", NAMESPACE_JCR);

    // add language
    root.setAttribute(JCR_LANGUAGE, languageKey, NAMESPACE_JCR);

    return doc;
  }

  private Element getXmlI18nValue(String escapedKey, String key, String value, boolean generatedKeyProperty) {
    Element valueNode = new Element(escapedKey);

    // add boiler plate
    valueNode.setAttribute(JCR_PRIMARY_TYPE, JCR_NODETYPE_FOLDER, NAMESPACE_JCR);
    valueNode.setAttribute(JCR_MIXIN_TYPES, "[" + StringUtils.join(SLING_MESSAGE_MIXIN_TYPE, ",") + "]", NAMESPACE_JCR);

    // add extra key attribute
    if (generatedKeyProperty) {
      valueNode.setAttribute(SLING_KEY, key, NAMESPACE_SLING);
    }

    // add actual i18n value
    valueNode.setAttribute(SLING_MESSAGE, value, NAMESPACE_SLING);

    return valueNode;
  }

  /**
   * Creates a valid node name. Replaces all chars not in a-z, A-Z and 0-9 or '_', '.' with '-'.
   * @param value String to be labelized.
   * @return The labelized string.
   */
  private static String validName(String value) {

    // replace some special chars first
    String text = value;
    text = StringUtils.replace(text, "ä", "ae");
    text = StringUtils.replace(text, "ö", "oe");
    text = StringUtils.replace(text, "ü", "ue");
    text = StringUtils.replace(text, "ß", "ss");

    // replace all invalid chars
    StringBuilder sb = new StringBuilder(text);
    for (int i = 0; i < sb.length(); i++) {
      char ch = sb.charAt(i);
      if (!((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
          || (ch == '_') || (ch == '.'))) {
        ch = '-';
        sb.setCharAt(i, ch);
      }
    }
    return sb.toString();
  }

  /**
   * Build i18n resource PROPERTIES.
   * @return JSON
   */
  public String getI18nPropertiesString() throws IOException {
    // Load all properties
    Properties i18nProps = new Properties();

    // add entries
    for (Entry<String, String> entry : properties.entrySet()) {
      String key = entry.getKey();
      String escapedKey = validName(key);
      i18nProps.put(escapedKey, entry.getValue());
    }

    try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
      i18nProps.store(outStream, null);
      // Property files are always ISO 8859 encoded
      return outStream.toString(StandardCharsets.ISO_8859_1.name());
    }
  }

}