/*
* Copyright 2008 Google Inc.
*
* 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.
*/
package com.google.gwt.i18n.rebind;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.i18n.client.PluralRule.PluralForm;
import com.google.gwt.i18n.shared.GwtLocale;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* AbstractResource serves the same purpose as java
* ResourceBundle/PropertyResourceBundle.
* <p>
* Each <code>Resource</code> belongs to a resource tree, indicated by the
* path attribute.
* <p>
* AbstractResource uses a Factory pattern rather than a single static method to
* load itself given an abstract string path.
* <p>
* One advanced feature which should not be used outside the core GWT system is
* that resources can have more than one parent, for instance pets_en_US could
* have pets_en as one parent and animals_en_US as another. The alternative
* parents have lower precedence than any primary parent. Each alternative
* parent is associated with a separate resource tree.
*/
public abstract class AbstractResource {
/**
* Exception indicating a required resource was not found.
*/
public static class MissingResourceException extends RuntimeException {
private String during;
private String key;
private String method;
private List<AbstractResource> searchedResources;
public MissingResourceException(String key,
List<AbstractResource> searchedResources) {
super("No resource found for key '" + key + "'");
this.key = key;
this.searchedResources = searchedResources;
}
public String getDuring() {
return during;
}
public String getKey() {
return key;
}
public String getMethod() {
return method;
}
public List<AbstractResource> getSearchedResources() {
return searchedResources;
}
public void setDuring(String during) {
this.during = during;
}
public void setMethod(String method) {
this.method = method;
}
}
/**
* Definition of a single entry for a resource.
*/
public interface ResourceEntry {
/**
* Retrieve a particular form for this entry.
*
* @param form form to retrieve (null for the default)
* @return null if the requested form is not present
*/
String getForm(String form);
/**
* Returns a list of forms associated with this entry.
*
* The default form (also the only form for anything other than messages
* with plural support) is always available and not present in this list.
*/
Collection<String> getForms();
/**
* Returns key for this entry (must not be null).
*/
String getKey();
}
/**
* Encapsulates an ordered set of resources to search for translations.
*/
public static class ResourceList extends AbstractList<AbstractResource> {
private List<AbstractResource> list = new ArrayList<AbstractResource>();
private Map<String, PluralForm[]> pluralForms = new HashMap<String, PluralForm[]>();
private Set<AbstractResource> set = new HashSet<AbstractResource>();
@Override
public boolean add(AbstractResource element) {
if (set.contains(element)) {
return false;
}
set.add(element);
return list.add(element);
}
@Override
public void add(int index, AbstractResource element) {
if (set.contains(element)) {
throw new IllegalArgumentException("Duplicate element");
}
set.add(element);
list.add(index, element);
}
/**
* Add all keys known by this ResourceList to the specified set.
*
* @param s set to add keys to
*/
public void addToKeySet(Set<String> s) {
for (AbstractResource resource : list) {
resource.addToKeySet(s);
}
}
/**
* From the list of locales matched for any resources in this resource list,
* choose the one that is least derived from the original search locale.
* @param logger logger to use
* @param locale originally requested locale
* @return least derived matched locale
*/
public GwtLocale findLeastDerivedLocale(TreeLogger logger,
GwtLocale locale) {
List<GwtLocale> searchList = locale.getCompleteSearchList();
Map<GwtLocale, Integer> derivedIndex = new HashMap<GwtLocale, Integer>();
for (int i = 0; i < searchList.size(); ++i) {
derivedIndex.put(searchList.get(i), i);
}
GwtLocale defaultLocale = LocaleUtils.getLocaleFactory().getDefault();
GwtLocale best = defaultLocale;
int bestIdx = Integer.MAX_VALUE;
for (int i = 0; i < list.size(); ++i) {
GwtLocale matchLocale = list.get(i).getMatchLocale();
Integer wrappedIdx = derivedIndex.get(matchLocale);
if (wrappedIdx == null) {
// We had an @DefaultLocale for a locale not present in this
// permutation -- treat it as the default locale.
wrappedIdx = derivedIndex.get(defaultLocale);
if (wrappedIdx == null) {
// shouldn't happen
assert false : "No default locale in search list";
continue;
}
}
int idx = wrappedIdx;
if (idx < bestIdx) {
bestIdx = idx;
best = matchLocale;
}
}
return best;
}
@Override
public AbstractResource get(int index) {
return list.get(index);
}
/**
* Returns the first AnnotationsResource containing a specified key.
*
* @param logger
* @param key
* @return first AnnotationsResource containing key, or null if none
*/
public AnnotationsResource getAnnotationsResource(TreeLogger logger,
String key) {
for (AbstractResource resource : list) {
if (resource instanceof AnnotationsResource
&& resource.keySet.contains(key)) {
return (AnnotationsResource) resource;
}
}
return null;
}
/**
* Get an entry from the first resource in this list containing a match.
*
* @param key
* @return a ResourceEntry instance
*/
public ResourceEntry getEntry(String key) {
for (AbstractResource resource : list) {
ResourceEntry e = resource.getEntry(key);
if (e != null) {
return e;
}
}
return null;
}
/**
* Returns the list of extensions available for a given key.
*
* @param key
* @return collection of extensions for the given key
*/
public Collection<String> getExtension(String key) {
Set<String> extensions = new HashSet<String>();
for (AbstractResource resource : list) {
extensions.addAll(resource.getExtensions(key));
}
return extensions;
}
/**
* Returns the list of plural forms for a given key.
*
* @param key
* @return array of plural forms.
*/
public PluralForm[] getPluralForms(String key) {
return pluralForms.get(key);
}
/**
* Returns a translation for a key, or throw an exception.
*
* @param key
* @return translated string for key
* @throws MissingResourceException
*/
public String getRequiredString(String key)
throws MissingResourceException {
String val = getString(key);
if (val == null) {
throw new MissingResourceException(key, list);
}
return val;
}
/**
* Returns a translation for a key/extension, or throw an exception.
*
* @param key
* @param ext key extension, null if none
* @return translated string for key
* @throws MissingResourceException
*/
public String getRequiredStringExt(String key, String ext)
throws MissingResourceException {
String val = getStringExt(key, ext);
if (val == null) {
throw new MissingResourceException(getExtendedKey(key, ext), list);
}
return val;
}
/**
* Returns a translation for a key, or null if not found.
*
* @param key
* @return translated string for key
*/
public String getString(String key) {
for (AbstractResource resource : list) {
String s = resource.getStringExt(key, null);
if (s != null) {
return s;
}
}
return null;
}
/**
* Returns a translation for a key/extension, or null if not found.
*
* @param key
* @param extension key extension, null if none
* @return translated string for key
*/
public String getStringExt(String key, String extension) {
for (AbstractResource resource : list) {
String s = resource.getStringExt(key, extension);
if (s != null) {
return s;
}
}
return null;
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public Iterator<AbstractResource> iterator() {
return list.iterator();
}
/**
* Returns set of keys present across all resources.
*/
public Set<String> keySet() {
Set<String> keySet = new HashSet<String>();
for (AbstractResource resource : list) {
keySet.addAll(resource.keySet());
}
return keySet;
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public AbstractResource remove(int index) {
AbstractResource element = list.remove(index);
set.remove(element);
return element;
}
/**
* Set the plural forms associated with a given message.
*
* @param key
* @param forms
*/
public void setPluralForms(String key, PluralForm[] forms) {
if (!pluralForms.containsKey(key)) {
pluralForms.put(key, forms);
}
}
@Override
public int size() {
return list.size();
}
}
/**
* Implementation of ResourceEntry that supports multiple forms per entry.
*/
protected static class MultipleFormEntry implements ResourceEntry {
private final String key;
private final Map<String, String> values = new HashMap<String, String>();
private final Set<String> forms = new HashSet<String>();
public MultipleFormEntry(String key) {
this.key = key;
}
public void addForm(String form, String value) {
values.put(form, value);
if (form != null) {
forms.add(form);
}
}
public String getForm(String form) {
return values.get(form);
}
public Collection<String> getForms() {
return forms;
}
public String getKey() {
return key;
}
}
/**
* A simple resource entry with no alternate forms, only a key and a value.
*/
protected static class SimpleEntry implements ResourceEntry {
private final String key;
private final String value;
public SimpleEntry(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SimpleEntry other = (SimpleEntry) obj;
return key.equals(other.key) && value.equals(other.value);
}
public String getForm(String form) {
return form != null ? null : value;
}
public Collection<String> getForms() {
return Collections.emptyList();
}
public String getKey() {
return key;
}
@Override
public int hashCode() {
return key.hashCode() + 31 * value.hashCode();
}
}
/**
* Error messages concerning missing keys should include the defined keys if
* the number of keys is below this threshold.
*/
public static final int REPORT_KEYS_THRESHOLD = 30;
protected static String getExtendedKey(String key, String extension) {
if (extension != null) {
key += '[' + extension + ']';
}
return key;
}
protected GwtLocale matchLocale;
private Set<String> keySet;
private String path;
public AbstractResource(GwtLocale matchLocale) {
this.matchLocale = matchLocale;
}
/**
* Returns an entry in this resource.
*
* @param key
* @return ResourceEntry instance
*/
public ResourceEntry getEntry(String key) {
String value = getString(key);
return value == null ? null : new SimpleEntry(key, value);
}
/**
* @param key
*/
public Collection<String> getExtensions(String key) {
return new ArrayList<String>();
}
/**
* Get a string and fail if not present.
*
* @param key
* @return the requested string
*/
public final String getRequiredString(String key) {
return getRequiredStringExt(key, null);
}
/**
* Get a string (with optional extension) and fail if not present.
*
* @param key
* @param extension
* @return the requested string
*/
public final String getRequiredStringExt(String key, String extension) {
String s = getStringExt(key, extension);
if (s == null) {
ArrayList<AbstractResource> list = new ArrayList<AbstractResource>();
list.add(this);
throw new MissingResourceException(key, list);
}
return s;
}
/**
* Get a key.
*
* @param key key to lookup
* @return the string for the given key or null if not found
* @see java.util.ResourceBundle#getString(java.lang.String)
*/
public final String getString(String key) {
return getStringExt(key, null);
}
/**
* Get a key with an extension. Identical to getString() if extension is null.
*
* @param key to lookup
* @param extension extension of the key, nullable
* @return string or null
*/
public abstract String getStringExt(String key, String extension);
/**
* Keys associated with this resource.
*
* @return keys
*/
public Set<String> keySet() {
if (keySet == null) {
keySet = new HashSet<String>();
addToKeySet(keySet);
}
return keySet;
}
/**
* Returns true if this resource has any keys.
*/
public boolean notEmpty() {
return !keySet.isEmpty();
}
@Override
public String toString() {
return "resource for " + path;
}
/**
* A multi-line representation of this object.
*
* @return verbose string
*/
public String toVerboseString() {
StringBuffer b = new StringBuffer();
toVerboseStringAux(0, b);
return b.toString();
}
abstract void addToKeySet(Set<String> s);
GwtLocale getMatchLocale() {
return matchLocale;
}
String getPath() {
return path;
}
void setPath(String path) {
this.path = path;
}
private void newLine(int indent, StringBuffer buf) {
buf.append("\n");
for (int i = 0; i < indent; i++) {
buf.append("\t");
}
}
private void toVerboseStringAux(int indent, StringBuffer buf) {
newLine(indent, buf);
buf.append(toString());
}
}