001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2016 ForgeRock AS. 015 */ 016package org.forgerock.util.i18n; 017 018import java.net.URI; 019import java.net.URISyntaxException; 020import java.util.MissingResourceException; 021import java.util.Objects; 022 023/** 024 * Represents a String which could be localizable. If it is localizable it needs to be in the following format, 025 * {@code i18n:bundle#key} which is a URI where: 026 * <ul> 027 * <li>{@code i18n:} is the scheme specifying that the string is localizable</li> 028 * <li>{@code bundle} is the path of the bundle in the classpath (optional, if missing, 029 * a {@link Class} has to be provided and will be used as resource bundle name)</li> 030 * <li>{@code key}, the fragment, is the key of the translated string</li> 031 * </ul> 032 * This class attempts to make the i18n work for an OSGi environment, by encapsulating the name of a resource bundle, 033 * the key in the resource bundle to use, and the {@code ClassLoader} in which the resource bundle can be found, in the 034 * assumption that when it comes to serializing this object, the calling code (e.g. HttpFrameworkServlet and the 035 * Grizzly HandlerAdapter) is in a different classloader and so will not have direct access to the resource bundle. 036 * <p> 037 * A default value {@code LocalizableString} can be provided so that if the key is not found in the bundle, another 038 * value can be specified, which could be either another bundle reference, or a plain value. 039 * </p> 040 */ 041public class LocalizableString { 042 043 /** 044 * A constant used to indicate a string should be translated. 045 */ 046 public static final String TRANSLATION_KEY_PREFIX = "i18n:"; 047 048 private final ClassLoader loader; 049 private final String value; 050 private final URI resource; 051 private final LocalizableString defaultValue; 052 053 /** 054 * String only constructor for non-localizable {@code String} values. 055 * @param value a string 056 */ 057 public LocalizableString(String value) { 058 this(value, (ClassLoader) null); 059 } 060 061 /** 062 * Constructor for potentially localizable {@code String}. 063 * If resource bundle name not provided in the {@code value}, then the provided {@code type} name will be 064 * used instead. 065 * @param value the String ({@literal i18n:#key.name} is accepted here) 066 * @param type class used to support relative resource bundle lookup (must not be {@code null}) 067 */ 068 public LocalizableString(String value, Class<?> type) { 069 // Integrates class name as the resource name in the value 070 this(value.replace(":#", ":" + type.getName().replace(".", "/") + "#"), type.getClassLoader()); 071 } 072 073 /** 074 * Constructor for potentially localizable {@code String}. 075 * @param value the String 076 * @param loader the {@code ClassLoader} where the string definition should be obtained 077 */ 078 public LocalizableString(String value, ClassLoader loader) { 079 this(value, loader, null); 080 } 081 082 /** 083 * Constructor for potentially localizable {@code String}. If a default value is not specified, if the {@code key} 084 * is a valid URI, its fragment will be used, and otherwise the whole {@code key} value will be used. 085 * @param key the localizable key 086 * @param loader the {@code ClassLoader} where the string definition should be obtained 087 * @param defaultValue the default value to use if not localizable. 088 */ 089 public LocalizableString(String key, ClassLoader loader, LocalizableString defaultValue) { 090 this.loader = loader; 091 this.value = key; 092 this.defaultValue = defaultValue; 093 094 URI resource = null; 095 if (value != null && value.startsWith(TRANSLATION_KEY_PREFIX) && loader != null) { 096 try { 097 resource = new URI(value); 098 } catch (URISyntaxException e) { 099 // 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}