View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  package org.forgerock.util.i18n;
17  
18  import java.net.URI;
19  import java.net.URISyntaxException;
20  import java.util.MissingResourceException;
21  import java.util.Objects;
22  
23  /**
24   * Represents a String which could be localizable. If it is localizable it needs to be in the following format,
25   * {@code i18n:bundle#key} which is a URI where:
26   * <ul>
27   *    <li>{@code i18n:} is the scheme specifying that the string is localizable</li>
28   *    <li>{@code bundle} is the path of the bundle in the classpath (optional, if missing,
29   *    a {@link Class} has to be provided and will be used as resource bundle name)</li>
30   *    <li>{@code key}, the fragment, is the key of the translated string</li>
31   * </ul>
32   * This class attempts to make the i18n work for an OSGi environment, by encapsulating the name of a resource bundle,
33   * the key in the resource bundle to use, and the {@code ClassLoader} in which the resource bundle can be found, in the
34   * assumption that when it comes to serializing this object, the calling code (e.g. HttpFrameworkServlet and the
35   * Grizzly HandlerAdapter) is in a different classloader and so will not have direct access to the resource bundle.
36   * <p>
37   *     A default value {@code LocalizableString} can be provided so that if the key is not found in the bundle, another
38   *     value can be specified, which could be either another bundle reference, or a plain value.
39   * </p>
40   */
41  public class LocalizableString {
42  
43      /**
44       * A constant used to indicate a string should be translated.
45       */
46      public static final String TRANSLATION_KEY_PREFIX = "i18n:";
47  
48      private final ClassLoader loader;
49      private final String value;
50      private final URI resource;
51      private final LocalizableString defaultValue;
52  
53      /**
54       * String only constructor for non-localizable {@code String} values.
55       * @param value a string
56       */
57      public LocalizableString(String value) {
58          this(value, (ClassLoader) null);
59      }
60  
61      /**
62       * Constructor for potentially localizable {@code String}.
63       * If resource bundle name not provided in the {@code value}, then the provided {@code type} name will be
64       * used instead.
65       * @param value the String ({@literal i18n:#key.name} is accepted here)
66       * @param type class used to support relative resource bundle lookup (must not be {@code null})
67       */
68      public LocalizableString(String value, Class<?> type) {
69          // Integrates class name as the resource name in the value
70          this(value.replace(":#", ":" + type.getName().replace(".", "/") + "#"), type.getClassLoader());
71      }
72  
73      /**
74       * Constructor for potentially localizable {@code String}.
75       * @param value the String
76       * @param loader the {@code ClassLoader} where the string definition should be obtained
77       */
78      public LocalizableString(String value, ClassLoader loader) {
79          this(value, loader, null);
80      }
81  
82      /**
83       * Constructor for potentially localizable {@code String}. If a default value is not specified, if the {@code key}
84       * is a valid URI, its fragment will be used, and otherwise the whole {@code key} value will be used.
85       * @param key the localizable key
86       * @param loader the {@code ClassLoader} where the string definition should be obtained
87       * @param defaultValue the default value to use if not localizable.
88       */
89      public LocalizableString(String key, ClassLoader loader, LocalizableString defaultValue) {
90          this.loader = loader;
91          this.value = key;
92          this.defaultValue = defaultValue;
93  
94          URI resource = null;
95          if (value != null && value.startsWith(TRANSLATION_KEY_PREFIX) && loader != null) {
96              try {
97                  resource = new URI(value);
98              } catch (URISyntaxException e) {
99                  // empty - use original value
100             }
101         }
102         this.resource = resource;
103     }
104 
105     /**
106      * Returns the contained string, translated if applicable.
107      * @param locales The preferred locales for the translation.
108      * @return the translated string
109      */
110     public String toTranslatedString(PreferredLocales locales) {
111         if (resource == null) {
112             return this.value;
113         } else {
114             try {
115                 return locales.getBundleInPreferredLocale(resource.getSchemeSpecificPart(), loader)
116                         .getString(resource.getFragment());
117             } catch (MissingResourceException e) {
118                 // the bundle wasn't found, so we use the default value, or return the fragment
119                 return defaultValue == null ? resource.getFragment() : defaultValue.toTranslatedString(locales);
120             }
121         }
122     }
123 
124     /**
125      * The default toString method. No translation is applied.
126      * @return the untranslated string value
127      */
128     public String toString() {
129         return defaultValue == null ? value : "[" + value + "], default [" + defaultValue + "]";
130     }
131 
132     /**
133      * The default equals operation.
134      * @param o another object
135      * @return true if the other object is equal
136      */
137     @Override
138     public boolean equals(Object o) {
139         if (this == o) {
140             return true;
141         }
142         if (o == null || getClass() != o.getClass()) {
143             return false;
144         }
145 
146         LocalizableString that = (LocalizableString) o;
147 
148         return value.equals(that.value) && Objects.equals(defaultValue, that.defaultValue);
149 
150     }
151 
152     /**
153      * Default hashcode implementation.
154      * @return the hashcode of the string
155      */
156     @Override
157     public int hashCode() {
158         return Objects.hash(value, defaultValue);
159     }
160 }