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 * @version $Id: ExceptionContext.java 1364388 2012-07-22 18:16:43Z tn $
038 */
039public class ExceptionContext implements Serializable {
040    /** Serializable version Id. */
041    private static final long serialVersionUID = -6024911025449780478L;
042    /**
043     * The throwable to which this context refers to.
044     */
045    private Throwable throwable;
046    /**
047     * Various informations that enrich the informative message.
048     */
049    private List<Localizable> msgPatterns;
050    /**
051     * Various informations that enrich the informative message.
052     * The arguments will replace the corresponding place-holders in
053     * {@link #msgPatterns}.
054     */
055    private List<Object[]> msgArguments;
056    /**
057     * Arbitrary context information.
058     */
059    private Map<String, Object> context;
060
061    /** Simple constructor.
062     * @param throwable the exception this context refers too
063     */
064    public ExceptionContext(final Throwable throwable) {
065        this.throwable = throwable;
066        msgPatterns    = new ArrayList<Localizable>();
067        msgArguments   = new ArrayList<Object[]>();
068        context        = new HashMap<String, Object>();
069    }
070
071    /** Get a reference to the exception to which the context relates.
072     * @return a reference to the exception to which the context relates
073     */
074    public Throwable getThrowable() {
075        return throwable;
076    }
077
078    /**
079     * Adds a message.
080     *
081     * @param pattern Message pattern.
082     * @param arguments Values for replacing the placeholders in the message
083     * pattern.
084     */
085    public void addMessage(Localizable pattern,
086                           Object ... arguments) {
087        msgPatterns.add(pattern);
088        msgArguments.add(ArgUtils.flatten(arguments));
089    }
090
091    /**
092     * Sets the context (key, value) pair.
093     * Keys are assumed to be unique within an instance. If the same key is
094     * assigned a new value, the previous one will be lost.
095     *
096     * @param key Context key (not null).
097     * @param value Context value.
098     */
099    public void setValue(String key, Object value) {
100        context.put(key, value);
101    }
102
103    /**
104     * Gets the value associated to the given context key.
105     *
106     * @param key Context key.
107     * @return the context value or {@code null} if the key does not exist.
108     */
109    public Object getValue(String key) {
110        return context.get(key);
111    }
112
113    /**
114     * Gets all the keys stored in the exception
115     *
116     * @return the set of keys.
117     */
118    public Set<String> getKeys() {
119        return context.keySet();
120    }
121
122    /**
123     * Gets the default message.
124     *
125     * @return the message.
126     */
127    public String getMessage() {
128        return getMessage(Locale.US);
129    }
130
131    /**
132     * Gets the message in the default locale.
133     *
134     * @return the localized message.
135     */
136    public String getLocalizedMessage() {
137        return getMessage(Locale.getDefault());
138    }
139
140    /**
141     * Gets the message in a specified locale.
142     *
143     * @param locale Locale in which the message should be translated.
144     * @return the localized message.
145     */
146    public String getMessage(final Locale locale) {
147        return buildMessage(locale, ": ");
148    }
149
150    /**
151     * Gets the message in a specified locale.
152     *
153     * @param locale Locale in which the message should be translated.
154     * @param separator Separator inserted between the message parts.
155     * @return the localized message.
156     */
157    public String getMessage(final Locale locale,
158                             final String separator) {
159        return buildMessage(locale, separator);
160    }
161
162    /**
163     * Builds a message string.
164     *
165     * @param locale Locale in which the message should be translated.
166     * @param separator Message separator.
167     * @return a localized message string.
168     */
169    private String buildMessage(Locale locale,
170                                String separator) {
171        final StringBuilder sb = new StringBuilder();
172        int count = 0;
173        final int len = msgPatterns.size();
174        for (int i = 0; i < len; i++) {
175            final Localizable pat = msgPatterns.get(i);
176            final Object[] args = msgArguments.get(i);
177            final MessageFormat fmt = new MessageFormat(pat.getLocalizedString(locale),
178                                                        locale);
179            sb.append(fmt.format(args));
180            if (++count < len) {
181                // Add a separator if there are other messages.
182                sb.append(separator);
183            }
184        }
185
186        return sb.toString();
187    }
188
189    /**
190     * Serialize this object to the given stream.
191     *
192     * @param out Stream.
193     * @throws IOException This should never happen.
194     */
195    private void writeObject(ObjectOutputStream out)
196        throws IOException {
197        out.writeObject(throwable);
198        serializeMessages(out);
199        serializeContext(out);
200    }
201    /**
202     * Deserialize this object from the given stream.
203     *
204     * @param in Stream.
205     * @throws IOException This should never happen.
206     * @throws ClassNotFoundException This should never happen.
207     */
208    private void readObject(ObjectInputStream in)
209        throws IOException,
210               ClassNotFoundException {
211        throwable = (Throwable) in.readObject();
212        deSerializeMessages(in);
213        deSerializeContext(in);
214    }
215
216    /**
217     * Serialize  {@link #msgPatterns} and {@link #msgArguments}.
218     *
219     * @param out Stream.
220     * @throws IOException This should never happen.
221     */
222    private void serializeMessages(ObjectOutputStream out)
223        throws IOException {
224        // Step 1.
225        final int len = msgPatterns.size();
226        out.writeInt(len);
227        // Step 2.
228        for (int i = 0; i < len; i++) {
229            final Localizable pat = msgPatterns.get(i);
230            // Step 3.
231            out.writeObject(pat);
232            final Object[] args = msgArguments.get(i);
233            final int aLen = args.length;
234            // Step 4.
235            out.writeInt(aLen);
236            for (int j = 0; j < aLen; j++) {
237                if (args[j] instanceof Serializable) {
238                    // Step 5a.
239                    out.writeObject(args[j]);
240                } else {
241                    // Step 5b.
242                    out.writeObject(nonSerializableReplacement(args[j]));
243                }
244            }
245        }
246    }
247
248    /**
249     * Deserialize {@link #msgPatterns} and {@link #msgArguments}.
250     *
251     * @param in Stream.
252     * @throws IOException This should never happen.
253     * @throws ClassNotFoundException This should never happen.
254     */
255    private void deSerializeMessages(ObjectInputStream in)
256        throws IOException,
257               ClassNotFoundException {
258        // Step 1.
259        final int len = in.readInt();
260        msgPatterns = new ArrayList<Localizable>(len);
261        msgArguments = new ArrayList<Object[]>(len);
262        // Step 2.
263        for (int i = 0; i < len; i++) {
264            // Step 3.
265            final Localizable pat = (Localizable) in.readObject();
266            msgPatterns.add(pat);
267            // Step 4.
268            final int aLen = in.readInt();
269            final Object[] args = new Object[aLen];
270            for (int j = 0; j < aLen; j++) {
271                // Step 5.
272                args[j] = in.readObject();
273            }
274            msgArguments.add(args);
275        }
276    }
277
278    /**
279     * Serialize {@link #context}.
280     *
281     * @param out Stream.
282     * @throws IOException This should never happen.
283     */
284    private void serializeContext(ObjectOutputStream out)
285        throws IOException {
286        // Step 1.
287        final int len = context.keySet().size();
288        out.writeInt(len);
289        for (String key : context.keySet()) {
290            // Step 2.
291            out.writeObject(key);
292            final Object value = context.get(key);
293            if (value instanceof Serializable) {
294                // Step 3a.
295                out.writeObject(value);
296            } else {
297                // Step 3b.
298                out.writeObject(nonSerializableReplacement(value));
299            }
300        }
301    }
302
303    /**
304     * Deserialize {@link #context}.
305     *
306     * @param in Stream.
307     * @throws IOException This should never happen.
308     * @throws ClassNotFoundException This should never happen.
309     */
310    private void deSerializeContext(ObjectInputStream in)
311        throws IOException,
312               ClassNotFoundException {
313        // Step 1.
314        final int len = in.readInt();
315        context = new HashMap<String, Object>();
316        for (int i = 0; i < len; i++) {
317            // Step 2.
318            final String key = (String) in.readObject();
319            // Step 3.
320            final Object value = in.readObject();
321            context.put(key, value);
322        }
323    }
324
325    /**
326     * Replaces a non-serializable object with an error message string.
327     *
328     * @param obj Object that does not implement the {@code Serializable}
329     * interface.
330     * @return a string that mentions which class could not be serialized.
331     */
332    private String nonSerializableReplacement(Object obj) {
333        return "[Object could not be serialized: " + obj.getClass().getName() + "]";
334    }
335}