001/*
002 * Copyright (c) 2004-2010, Kohsuke Kawaguchi
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
006 * that the following conditions are met:
007 *
008 *     * Redistributions of source code must retain the above copyright notice, this list of
009 *       conditions and the following disclaimer.
010 *     * Redistributions in binary form must reproduce the above copyright notice, this list of
011 *       conditions and the following disclaimer in the documentation and/or other materials
012 *       provided with the distribution.
013 *
014 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
015 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
016 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
017 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
018 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
019 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
020 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
021 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022 */
023
024package org.kohsuke.stapler.jelly;
025
026import org.kohsuke.stapler.MetaClass;
027import org.kohsuke.stapler.WebApp;
028
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.StringWriter;
032import java.net.URL;
033import java.text.MessageFormat;
034import java.util.Collections;
035import java.util.HashMap;
036import java.util.Locale;
037import java.util.Map;
038import java.util.Properties;
039import java.util.concurrent.ConcurrentHashMap;
040
041/**
042 * Cache of localization strings.
043 * 
044 * @author Kohsuke Kawaguchi
045 */
046public class ResourceBundle {
047    /**
048     * URL to load message resources from, except the ".properties" suffix.
049     * <p>
050     * This is normally something like <tt>file://path/to/somewhere/org/acme/Foo</tt>.
051     */
052    private final String baseName;
053
054    /**
055     * Loaded messages.
056     */
057    private final Map<String,Properties> resources = new ConcurrentHashMap<String,Properties>();
058
059    public ResourceBundle(String baseName) {
060        this.baseName = baseName;
061    }
062
063    public String getBaseName() {
064        return baseName;
065    }
066
067    public String format(Locale locale, String key, Object... args) {
068        String str = getFormatString(locale, key);
069        if(str==null)
070            // see http://www.nabble.com/i18n-and-l10n-problems-td16004047.html for more discussion
071            // return MessageFormat.format(key,args);
072            return key;
073
074        return MessageFormat.format(str,args);
075    }
076
077    /**
078     * Gets the format string for the given key.
079     * <p>
080     * This method performs a search so that a look up for "pt_BR" would delegate
081     * to "pt" then "" (the no-locale locale.)
082     */
083    public String getFormatString(Locale locale, String key) {
084        String[] suffixes = toStrings(locale);
085
086        while(true) {
087            for (int i=0; i<suffixes.length; i++) {
088                String suffix = suffixes[i];
089                String msg = get(suffix).getProperty(key);
090                if(msg!=null && msg.length()>0)
091                    // ignore a definition without value, because stapler:i18n generates
092                    // value-less definitions
093                    return msg;
094
095                int idx = suffix.lastIndexOf('_');
096                if(idx<0)   // failed to find
097                    return null;
098                suffixes[i] = suffix.substring(0,idx);
099            }
100        }
101    }
102
103    /**
104     * Works like {@link #getFormatString(Locale, String)} except there's no
105     * searching up the delegation chain.
106     */
107    public String getFormatStringWithoutDefaulting(Locale locale, String key) {
108        for (String s : toStrings(locale)) {
109            String msg = get(s).getProperty(key);
110            if(msg!=null && msg.length()>0)
111                return msg;
112        }
113        return null;
114    }
115
116    /**
117     * Some language codes have changed over time, such as Hebrew from iw to he.
118     * This method returns all such variations in an array.
119     *
120     * @see Locale#getLanguage()
121     */
122    private String[] toStrings(Locale l) {
123        String v = ISO639_MAP.get(l.getLanguage());
124        if (v==null)
125            return new String[]{'_'+l.toString()};
126        else
127            return new String[]{'_'+l.toString(),
128                                '_'+v+l.toString().substring(2)};
129    }
130
131    protected void clearCache() {
132        resources.clear();
133    }
134
135    protected Properties get(String key) {
136        Properties props;
137
138        if(!MetaClass.NO_CACHE) {
139            props = resources.get(key);
140            if(props!=null)     return props;
141        }
142
143        // attempt to load
144        props = new Properties();
145        String url = baseName + key + ".properties";
146        InputStream in=null;
147        try {
148            in = new URL(url).openStream();
149            // an user reported that on IBM JDK, URL.openStream
150            // returns null instead of IOException.
151            // see http://www.nabble.com/WAS---Hudson-tt16026561.html
152        } catch (IOException e) {
153            // failed.
154        }
155
156        if(in!=null) {
157            try {
158                try {
159                    props.load(in);
160                } finally {
161                    in.close();
162                }
163            } catch (IOException e) {
164                throw new Error("Failed to load "+url,e);
165            }
166        }
167
168        resources.put(key,wrapUp(key.length()>0 ? key.substring(1) : "",props));
169        return props;
170    }
171
172    /**
173     * Interception point for property loading.
174     */
175    protected Properties wrapUp(String locale, Properties props) {
176        return props;
177    }
178
179    @Override
180    public boolean equals(Object o) {
181        if (this == o) return true;
182        if (o == null || getClass() != o.getClass()) return false;
183
184        ResourceBundle that = (ResourceBundle) o;
185        return baseName.equals(that.baseName);
186    }
187
188    @Override
189    public int hashCode() {
190        return baseName.hashCode();
191    }
192
193    public static ResourceBundle load(URL jellyUrl) {
194        return load(jellyUrl.toExternalForm());
195    }
196
197    /**
198     * Loads the resource bundle associated with the Jelly script.
199     */
200    public static ResourceBundle load(String jellyUrl) {
201        if(jellyUrl.endsWith(".jelly"))    // cut the trailing .jelly
202            jellyUrl = jellyUrl.substring(0,jellyUrl.length()-".jelly".length());
203
204        JellyFacet facet = WebApp.getCurrent().getFacet(JellyFacet.class);
205        return facet.resourceBundleFactory.create(jellyUrl);
206    }
207
208    /**
209     * JDK internally converts new ISO-639 code back to old code. This table provides reverse mapping.
210     */
211    private static final Map<String,String> ISO639_MAP = new HashMap<String, String>();
212
213    static {
214        ISO639_MAP.put("iw","he");
215        ISO639_MAP.put("ji","yi");
216        ISO639_MAP.put("in","id");
217    }
218}