001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.math3.exception.util;
018
019import java.util.List;
020import java.util.ArrayList;
021import java.util.Set;
022import java.util.Map;
023import java.io.IOException;
024import java.io.Serializable;
025import java.io.ObjectOutputStream;
026import java.io.ObjectInputStream;
027import java.util.HashMap;
028import java.text.MessageFormat;
029import java.util.Locale;
030
031/**
032 * Class that contains the actual implementation of the functionality mandated
033 * by the {@link ExceptionContext} interface.
034 * All Commons Math exceptions delegate the interface's methods to this class.
035 *
036 * @since 3.0
037 */
038public class ExceptionContext implements Serializable {
039    /** Serializable version Id. */
040    private static final long serialVersionUID = -6024911025449780478L;
041    /**
042     * The throwable to which this context refers to.
043     */
044    private Throwable throwable;
045    /**
046     * Various informations that enrich the informative message.
047     */
048    private List<Localizable> msgPatterns;
049    /**
050     * Various informations that enrich the informative message.
051     * The arguments will replace the corresponding place-holders in
052     * {@link #msgPatterns}.
053     */
054    private List<Object[]> msgArguments;
055    /**
056     * Arbitrary context information.
057     */
058    private Map<String, Object> context;
059
060    /** Simple constructor.
061     * @param throwable the exception this context refers too
062     */
063    public ExceptionContext(final Throwable throwable) {
064        this.throwable = throwable;
065        msgPatterns    = new ArrayList<Localizable>();
066        msgArguments   = new ArrayList<Object[]>();
067        context        = new HashMap<String, Object>();
068    }
069
070    /** Get a reference to the exception to which the context relates.
071     * @return a reference to the exception to which the context relates
072     */
073    public Throwable getThrowable() {
074        return throwable;
075    }
076
077    /**
078     * Adds a message.
079     *
080     * @param pattern Message pattern.
081     * @param arguments Values for replacing the placeholders in the message
082     * pattern.
083     */
084    public void addMessage(Localizable pattern,
085                           Object ... arguments) {
086        msgPatterns.add(pattern);
087        msgArguments.add(ArgUtils.flatten(arguments));
088    }
089
090    /**
091     * Sets the context (key, value) pair.
092     * Keys are assumed to be unique within an instance. If the same key is
093     * assigned a new value, the previous one will be lost.
094     *
095     * @param key Context key (not null).
096     * @param value Context value.
097     */
098    public void setValue(String key, Object value) {
099        context.put(key, value);
100    }
101
102    /**
103     * Gets the value associated to the given context key.
104     *
105     * @param key Context key.
106     * @return the context value or {@code null} if the key does not exist.
107     */
108    public Object getValue(String key) {
109        return context.get(key);
110    }
111
112    /**
113     * Gets all the keys stored in the exception
114     *
115     * @return the set of keys.
116     */
117    public Set<String> getKeys() {
118        return context.keySet();
119    }
120
121    /**
122     * Gets the default message.
123     *
124     * @return the message.
125     */
126    public String getMessage() {
127        return getMessage(Locale.US);
128    }
129
130    /**
131     * Gets the message in the default locale.
132     *
133     * @return the localized message.
134     */
135    public String getLocalizedMessage() {
136        return getMessage(Locale.getDefault());
137    }
138
139    /**
140     * Gets the message in a specified locale.
141     *
142     * @param locale Locale in which the message should be translated.
143     * @return the localized message.
144     */
145    public String getMessage(final Locale locale) {
146        return buildMessage(locale, ": ");
147    }
148
149    /**
150     * Gets the message in a specified locale.
151     *
152     * @param locale Locale in which the message should be translated.
153     * @param separator Separator inserted between the message parts.
154     * @return the localized message.
155     */
156    public String getMessage(final Locale locale,
157                             final String separator) {
158        return buildMessage(locale, separator);
159    }
160
161    /**
162     * Builds a message string.
163     *
164     * @param locale Locale in which the message should be translated.
165     * @param separator Message separator.
166     * @return a localized message string.
167     */
168    private String buildMessage(Locale locale,
169                                String separator) {
170        final StringBuilder sb = new StringBuilder();
171        int count = 0;
172        final int len = msgPatterns.size();
173        for (int i = 0; i < len; i++) {
174            final Localizable pat = msgPatterns.get(i);
175            final Object[] args = msgArguments.get(i);
176            final MessageFormat fmt = new MessageFormat(pat.getLocalizedString(locale),
177                                                        locale);
178            sb.append(fmt.format(args));
179            if (++count < len) {
180                // Add a separator if there are other messages.
181                sb.append(separator);
182            }
183        }
184
185        return sb.toString();
186    }
187
188    /**
189     * Serialize this object to the given stream.
190     *
191     * @param out Stream.
192     * @throws IOException This should never happen.
193     */
194    private void writeObject(ObjectOutputStream out)
195        throws IOException {
196        out.writeObject(throwable);
197        serializeMessages(out);
198        serializeContext(out);
199    }
200    /**
201     * Deserialize this object from the given stream.
202     *
203     * @param in Stream.
204     * @throws IOException This should never happen.
205     * @throws ClassNotFoundException This should never happen.
206     */
207    private void readObject(ObjectInputStream in)
208        throws IOException,
209               ClassNotFoundException {
210        throwable = (Throwable) in.readObject();
211        deSerializeMessages(in);
212        deSerializeContext(in);
213    }
214
215    /**
216     * Serialize  {@link #msgPatterns} and {@link #msgArguments}.
217     *
218     * @param out Stream.
219     * @throws IOException This should never happen.
220     */
221    private void serializeMessages(ObjectOutputStream out)
222        throws IOException {
223        // Step 1.
224        final int len = msgPatterns.size();
225        out.writeInt(len);
226        // Step 2.
227        for (int i = 0; i < len; i++) {
228            final Localizable pat = msgPatterns.get(i);
229            // Step 3.
230            out.writeObject(pat);
231            final Object[] args = msgArguments.get(i);
232            final int aLen = args.length;
233            // Step 4.
234            out.writeInt(aLen);
235            for (int j = 0; j < aLen; j++) {
236                if (args[j] instanceof Serializable) {
237                    // Step 5a.
238                    out.writeObject(args[j]);
239                } else {
240                    // Step 5b.
241                    out.writeObject(nonSerializableReplacement(args[j]));
242                }
243            }
244        }
245    }
246
247    /**
248     * Deserialize {@link #msgPatterns} and {@link #msgArguments}.
249     *
250     * @param in Stream.
251     * @throws IOException This should never happen.
252     * @throws ClassNotFoundException This should never happen.
253     */
254    private void deSerializeMessages(ObjectInputStream in)
255        throws IOException,
256               ClassNotFoundException {
257        // Step 1.
258        final int len = in.readInt();
259        msgPatterns = new ArrayList<Localizable>(len);
260        msgArguments = new ArrayList<Object[]>(len);
261        // Step 2.
262        for (int i = 0; i < len; i++) {
263            // Step 3.
264            final Localizable pat = (Localizable) in.readObject();
265            msgPatterns.add(pat);
266            // Step 4.
267            final int aLen = in.readInt();
268            final Object[] args = new Object[aLen];
269            for (int j = 0; j < aLen; j++) {
270                // Step 5.
271                args[j] = in.readObject();
272            }
273            msgArguments.add(args);
274        }
275    }
276
277    /**
278     * Serialize {@link #context}.
279     *
280     * @param out Stream.
281     * @throws IOException This should never happen.
282     */
283    private void serializeContext(ObjectOutputStream out)
284        throws IOException {
285        // Step 1.
286        final int len = context.size();
287        out.writeInt(len);
288        for (Map.Entry<String, Object> entry : context.entrySet()) {
289            // Step 2.
290            out.writeObject(entry.getKey());
291            final Object value = entry.getValue();
292            if (value instanceof Serializable) {
293                // Step 3a.
294                out.writeObject(value);
295            } else {
296                // Step 3b.
297                out.writeObject(nonSerializableReplacement(value));
298            }
299        }
300    }
301
302    /**
303     * Deserialize {@link #context}.
304     *
305     * @param in Stream.
306     * @throws IOException This should never happen.
307     * @throws ClassNotFoundException This should never happen.
308     */
309    private void deSerializeContext(ObjectInputStream in)
310        throws IOException,
311               ClassNotFoundException {
312        // Step 1.
313        final int len = in.readInt();
314        context = new HashMap<String, Object>();
315        for (int i = 0; i < len; i++) {
316            // Step 2.
317            final String key = (String) in.readObject();
318            // Step 3.
319            final Object value = in.readObject();
320            context.put(key, value);
321        }
322    }
323
324    /**
325     * Replaces a non-serializable object with an error message string.
326     *
327     * @param obj Object that does not implement the {@code Serializable}
328     * interface.
329     * @return a string that mentions which class could not be serialized.
330     */
331    private String nonSerializableReplacement(Object obj) {
332        return "[Object could not be serialized: " + obj.getClass().getName() + "]";
333    }
334}