001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.ioc.util;
014
015import org.apache.tapestry5.ioc.internal.util.InternalUtils;
016
017import java.util.Iterator;
018import java.util.Locale;
019import java.util.NoSuchElementException;
020
021/**
022 * Generates name variations for a given file name or path and a locale. The name variations
023 * are provided in most-specific to least-specific order, so for a path of "Base.ext" and a Locale
024 * of "en_US", the generated names would be "Base_en_US.ext", "Base_en.ext", "Base.ext".
025 *
026 * Implements Iterable, so a LocalizedNameGenerator may be used directly in a for loop.
027 *
028 * This class is not threadsafe.
029 * 
030 * @since 5.3
031 */
032public class LocalizedNameGenerator implements Iterator<String>, Iterable<String>
033{
034    private final int baseNameLength;
035
036    private final String suffix;
037
038    private final StringBuilder builder;
039
040    private final String language;
041
042    private final String country;
043
044    private final String variant;
045
046    private int state;
047
048    private int prevState;
049
050    private static final int INITIAL = 0;
051
052    private static final int LCV = 1;
053
054    private static final int LC = 2;
055
056    private static final int LV = 3;
057
058    private static final int L = 4;
059
060    private static final int BARE = 5;
061
062    private static final int EXHAUSTED = 6;
063
064    /**
065     * Creates a new generator for the given path and locale.
066     * 
067     * @param path
068     *            non-blank path
069     * @param locale
070     *            non-null locale
071     */
072    public LocalizedNameGenerator(String path, Locale locale)
073    {
074        assert InternalUtils.isNonBlank(path);
075        assert locale != null;
076
077        int dotx = path.lastIndexOf('.');
078
079        // When there is no dot in the name, pretend it exists after the
080        // end of the string. The locale extensions will be tacked on there.
081
082        if (dotx == -1)
083            dotx = path.length();
084
085        String baseName = path.substring(0, dotx);
086
087        suffix = path.substring(dotx);
088
089        baseNameLength = dotx;
090
091        language = locale.getLanguage();
092        country = locale.getCountry();
093        variant = locale.getVariant();
094
095        state = INITIAL;
096        prevState = INITIAL;
097
098        builder = new StringBuilder(baseName);
099
100        advance();
101    }
102
103    private void advance()
104    {
105        prevState = state;
106
107        while (state != EXHAUSTED)
108        {
109            state++;
110
111            switch (state)
112            {
113                case LCV:
114
115                    if (InternalUtils.isBlank(variant))
116                        continue;
117
118                    return;
119
120                case LC:
121
122                    if (InternalUtils.isBlank(country))
123                        continue;
124
125                    return;
126
127                case LV:
128
129                    // If country is null, then we've already generated this string
130                    // as state LCV and we can continue directly to state L
131
132                    if (InternalUtils.isBlank(variant) || InternalUtils.isBlank(country))
133                        continue;
134
135                    return;
136
137                case L:
138
139                    if (InternalUtils.isBlank(language))
140                        continue;
141
142                    return;
143
144                case BARE:
145                default:
146                    return;
147            }
148        }
149    }
150
151    /**
152     * Returns true if there are more name variants to be returned, false otherwise.
153     */
154
155    @Override
156    public boolean hasNext()
157    {
158        return state != EXHAUSTED;
159    }
160
161    /**
162     * Returns the next localized variant.
163     * 
164     * @throws NoSuchElementException
165     *             if all variants have been returned.
166     */
167
168    @Override
169    public String next()
170    {
171        if (state == EXHAUSTED)
172            throw new NoSuchElementException();
173
174        String result = build();
175
176        advance();
177
178        return result;
179    }
180
181    private String build()
182    {
183        builder.setLength(baseNameLength);
184
185        if (state == LC || state == LCV || state == L || state == LV)
186        {
187            builder.append('_');
188            builder.append(language);
189        }
190
191        // For LV, we want two underscores between language
192        // and variant.
193
194        if (state == LC || state == LCV || state == LV)
195        {
196            builder.append('_');
197
198            if (state != LV)
199                builder.append(country);
200        }
201
202        if (state == LV || state == LCV)
203        {
204            builder.append('_');
205            builder.append(variant);
206        }
207
208        if (suffix != null)
209            builder.append(suffix);
210
211        return builder.toString();
212    }
213
214    public Locale getCurrentLocale()
215    {
216        switch (prevState)
217        {
218            case LCV:
219
220                return new Locale(language, country, variant);
221
222            case LC:
223
224                return new Locale(language, country, "");
225
226            case LV:
227
228                return new Locale(language, "", variant);
229
230            case L:
231
232                return new Locale(language, "", "");
233
234            default:
235                return null;
236        }
237    }
238
239    /**
240     * @throws UnsupportedOperationException
241     */
242    @Override
243    public void remove()
244    {
245        throw new UnsupportedOperationException();
246    }
247
248    /**
249     * So that LNG may be used with the for loop.
250     */
251    @Override
252    public Iterator<String> iterator()
253    {
254        return this;
255    }
256
257}