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     */
017    package org.apache.commons.math3.exception.util;
018    
019    import java.util.List;
020    import java.util.ArrayList;
021    import java.util.Set;
022    import java.util.Map;
023    import java.io.IOException;
024    import java.io.Serializable;
025    import java.io.ObjectOutputStream;
026    import java.io.ObjectInputStream;
027    import java.util.HashMap;
028    import java.text.MessageFormat;
029    import 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     */
039    public 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    }