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 Copyrighted [year] [name of copyright owner]".
013 *
014 *      Copyright 2007-2009 Sun Microsystems, Inc.
015 *      Portions copyright 2011 ForgeRock AS
016 */
017
018package org.forgerock.i18n;
019
020import java.io.Serializable;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Locale;
024
025/**
026 * A mutable sequence of localizable messages and their parameters. As messages
027 * are appended they are translated to their string representation for storage
028 * using the locale specified in the constructor.
029 * <p>
030 * Note that before you use this class you should consider whether it is
031 * appropriate. In general composing messages by appending message to each other
032 * may not produce a message that is formatted appropriately for all locales.
033 * <p>
034 * It is usually better to create messages by composition. In other words you
035 * should create a base message that contains one or more string argument
036 * specifiers (%s) and define other message objects to use as replacement
037 * variables. In this way language translators have a change to reformat the
038 * message for a particular locale if necessary.
039 *
040 * @see LocalizableMessage
041 */
042public final class LocalizableMessageBuilder implements Appendable,
043        CharSequence, Serializable {
044    /**
045     * Generated serialization ID.
046     */
047    private static final long serialVersionUID = -3292823563904285315L;
048
049    // Used internally to store appended messages.
050    private final List<LocalizableMessage> messages = new LinkedList<LocalizableMessage>();
051
052    /**
053     * Creates a new message builder whose content is initially empty.
054     */
055    public LocalizableMessageBuilder() {
056        // Nothing to do.
057    }
058
059    /**
060     * Creates a new message builder whose content is initially equal to the
061     * provided message.
062     *
063     * @param message
064     *            The initial content of the message builder.
065     * @throws NullPointerException
066     *             If {@code message} was {@code null}.
067     */
068    public LocalizableMessageBuilder(final LocalizableMessage message) {
069        append(message);
070    }
071
072    /**
073     * Creates a new message builder whose content is initially equal to the
074     * provided message builder.
075     *
076     * @param builder
077     *            The initial content of the message builder.
078     * @throws NullPointerException
079     *             If {@code builder} was {@code null}.
080     */
081    public LocalizableMessageBuilder(final LocalizableMessageBuilder builder) {
082        for (final LocalizableMessage message : builder.messages) {
083            this.messages.add(message);
084        }
085    }
086
087    /**
088     * Creates a new message builder whose content is initially equal to the
089     * {@code String} representation of the provided {@code Object}.
090     *
091     * @param object
092     *            The initial content of the message builder, may be
093     *            {@code null}.
094     */
095    public LocalizableMessageBuilder(final Object object) {
096        append(object);
097    }
098
099    /**
100     * Appends the provided character to this message builder.
101     *
102     * @param c
103     *            The character to be appended.
104     * @return A reference to this message builder.
105     */
106    public LocalizableMessageBuilder append(final char c) {
107        return append(LocalizableMessage.valueOf(c));
108    }
109
110    /**
111     * Appends the provided character sequence to this message builder.
112     *
113     * @param cs
114     *            The character sequence to be appended.
115     * @return A reference to this message builder.
116     * @throws NullPointerException
117     *             If {@code cs} was {@code null}.
118     */
119    public LocalizableMessageBuilder append(final CharSequence cs) {
120        if (cs == null) {
121            throw new NullPointerException("cs was null");
122        }
123
124        return append((Object) cs);
125    }
126
127    /**
128     * Appends a subsequence of the provided character sequence to this message
129     * builder.
130     * <p>
131     * An invocation of this method of the form {@code append(cs, start, end)},
132     * behaves in exactly the same way as the invocation
133     *
134     * <pre>
135     * append(cs.subSequence(start, end))
136     * </pre>
137     *
138     * @param cs
139     *            The character sequence to be appended.
140     * @param start
141     *            The index of the first character in the subsequence.
142     * @param end
143     *            The index of the character following the last character in the
144     *            subsequence.
145     * @return A reference to this message builder.
146     * @throws IndexOutOfBoundsException
147     *             If {@code start} or {@code end} are negative, {@code start}
148     *             is greater than {@code end}, or {@code end} is greater than
149     *             {@code csq.length()}.
150     * @throws NullPointerException
151     *             If {@code cs} was {@code null}.
152     */
153    public LocalizableMessageBuilder append(final CharSequence cs,
154            final int start, final int end) {
155        return append(cs.subSequence(start, end));
156    }
157
158    /**
159     * Appends the provided integer to this message builder.
160     *
161     * @param value
162     *            The integer to be appended.
163     * @return A reference to this message builder.
164     */
165    public LocalizableMessageBuilder append(final int value) {
166        return append(LocalizableMessage.valueOf(value));
167    }
168
169    /**
170     * Appends the provided message to this message builder.
171     *
172     * @param message
173     *            The message to be appended.
174     * @return A reference to this message builder.
175     * @throws NullPointerException
176     *             If {@code message} was {@code null}.
177     */
178    public LocalizableMessageBuilder append(final LocalizableMessage message) {
179        if (message == null) {
180            throw new NullPointerException("message was null");
181        }
182
183        messages.add(message);
184        return this;
185    }
186
187    /**
188     * Appends the {@code String} representation of the provided {@code Object}
189     * to this message builder.
190     *
191     * @param object
192     *            The object to be appended, may be {@code null}.
193     * @return A reference to this message builder.
194     */
195    public LocalizableMessageBuilder append(final Object object) {
196        return append(LocalizableMessage.valueOf(object));
197    }
198
199    /**
200     * Returns the {@code char} value at the specified index of the
201     * {@code String} representation of this message builder in the default
202     * locale.
203     *
204     * @param index
205     *            The index of the {@code char} value to be returned.
206     * @return The specified {@code char} value.
207     * @throws IndexOutOfBoundsException
208     *             If the {@code index} argument is negative or not less than
209     *             {@code length()}.
210     */
211    public char charAt(final int index) {
212        return charAt(Locale.getDefault(), index);
213    }
214
215    /**
216     * Returns the {@code char} value at the specified index of the
217     * {@code String} representation of this message builder in the specified
218     * locale.
219     *
220     * @param locale
221     *            The locale.
222     * @param index
223     *            The index of the {@code char} value to be returned.
224     * @return The specified {@code char} value.
225     * @throws IndexOutOfBoundsException
226     *             If the {@code index} argument is negative or not less than
227     *             {@code length()}.
228     * @throws NullPointerException
229     *             If {@code locale} was {@code null}.
230     */
231    public char charAt(final Locale locale, final int index) {
232        return toString(locale).charAt(index);
233    }
234
235    /**
236     * Returns the length of the {@code String} representation of this message
237     * builder in the default locale.
238     *
239     * @return The length of the {@code String} representation of this message
240     *         builder in the default locale.
241     */
242    public int length() {
243        return length(Locale.getDefault());
244    }
245
246    /**
247     * Returns the length of the {@code String} representation of this message
248     * builder in the specified locale.
249     *
250     * @param locale
251     *            The locale.
252     * @return The length of the {@code String} representation of this message
253     *         builder in the specified locale.
254     * @throws NullPointerException
255     *             If {@code locale} was {@code null}.
256     */
257    public int length(final Locale locale) {
258        return toString(locale).length();
259    }
260
261    /**
262     * Returns a new {@code CharSequence} which is a subsequence of the
263     * {@code String} representation of this message builder in the default
264     * locale. The subsequence starts with the {@code char} value at the
265     * specified index and ends with the {@code char} value at index
266     * {@code end - 1} . The length (in {@code char}s) of the returned sequence
267     * is {@code end - start}, so if {@code start == end} then an empty sequence
268     * is returned.
269     *
270     * @param start
271     *            The start index, inclusive.
272     * @param end
273     *            The end index, exclusive.
274     * @return The specified subsequence.
275     * @throws IndexOutOfBoundsException
276     *             If {@code start} or {@code end} are negative, if {@code end}
277     *             is greater than {@code length()}, or if {@code start} is
278     *             greater than {@code end}.
279     */
280    public CharSequence subSequence(final int start, final int end) {
281        return subSequence(Locale.getDefault(), start, end);
282    }
283
284    /**
285     * Returns a new {@code CharSequence} which is a subsequence of the
286     * {@code String} representation of this message builder in the specified
287     * locale. The subsequence starts with the {@code char} value at the
288     * specified index and ends with the {@code char} value at index
289     * {@code end - 1} . The length (in {@code char}s) of the returned sequence
290     * is {@code end - start}, so if {@code start == end} then an empty sequence
291     * is returned.
292     *
293     * @param locale
294     *            The locale.
295     * @param start
296     *            The start index, inclusive.
297     * @param end
298     *            The end index, exclusive.
299     * @return The specified subsequence.
300     * @throws IndexOutOfBoundsException
301     *             If {@code start} or {@code end} are negative, if {@code end}
302     *             is greater than {@code length()}, or if {@code start} is
303     *             greater than {@code end}.
304     * @throws NullPointerException
305     *             If {@code locale} was {@code null}.
306     */
307    public CharSequence subSequence(final Locale locale, final int start,
308            final int end) {
309        return toString(locale).subSequence(start, end);
310    }
311
312    /**
313     * Returns the {@link LocalizableMessage} representation of this message
314     * builder. Subsequent changes to this message builder will not modify the
315     * returned {@code LocalizableMessage}.
316     *
317     * @return The {@code LocalizableMessage} representation of this message
318     *         builder.
319     */
320    public LocalizableMessage toMessage() {
321        if (messages.isEmpty()) {
322            return LocalizableMessage.EMPTY;
323        }
324
325        final int sz = messages.size();
326        final StringBuffer fmtString = new StringBuffer(sz * 2);
327        for (int i = 0; i < sz; i++) {
328            fmtString.append("%s");
329        }
330
331        return LocalizableMessage.raw(fmtString, messages.toArray());
332    }
333
334    /**
335     * Returns the {@code String} representation of this message builder in the
336     * default locale.
337     *
338     * @return The {@code String} representation of this message builder.
339     */
340    @Override
341    public String toString() {
342        return toString(Locale.getDefault());
343    }
344
345    /**
346     * Returns the {@code String} representation of this message builder in the
347     * specified locale.
348     *
349     * @param locale
350     *            The locale.
351     * @return The {@code String} representation of this message builder.
352     * @throws NullPointerException
353     *             If {@code locale} was {@code null}.
354     */
355    public String toString(final Locale locale) {
356        final StringBuilder builder = new StringBuilder();
357        for (final LocalizableMessage message : messages) {
358            builder.append(message.toString(locale));
359        }
360        return builder.toString();
361    }
362
363}