1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.awt.datatransfer.DataFlavor;
21 import java.io.BufferedInputStream;
22 import java.io.BufferedOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.ObjectInputStream;
27 import java.io.ObjectOutputStream;
28 import java.io.ObjectStreamClass;
29 import java.io.OutputStream;
30 import java.io.OutputStreamWriter;
31 import java.io.Serializable;
32 import java.lang.reflect.Type;
33 import java.util.Locale;
34 import java.util.Map;
35
36 import com.google.gson.Gson;
37 import com.google.gson.GsonBuilder;
38 import com.google.gson.JsonElement;
39 import com.google.gson.JsonPrimitive;
40 import com.google.gson.JsonSerializationContext;
41 import com.google.gson.JsonSerializer;
42 import com.thoughtworks.xstream.XStream;
43 import com.thoughtworks.xstream.converters.collections.MapConverter;
44 import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
45 import com.thoughtworks.xstream.io.xml.CompactWriter;
46 import com.thoughtworks.xstream.security.NoTypePermission;
47 import com.thoughtworks.xstream.security.NullPermission;
48 import com.thoughtworks.xstream.security.PrimitiveTypePermission;
49
50 /**
51  * Liste des formats de transport entre un serveur de collecte et une application monitorée
52  * (hors protocole à priori http).
53  * @author Emeric Vernat
54  */

55 public enum TransportFormat {
56     /**
57      * Sérialisation java.
58      */

59     SERIALIZED(DataFlavor.javaSerializedObjectMimeType),
60
61     /**
62      * XML (avec <a href='http://x-stream.github.io/'>XStream</a> / XPP).
63      */

64     XML("text/xml; charset=utf-8"),
65
66     /**
67      * JSON (écriture en JSON avec <a href='http://x-stream.github.io/'>XStream</a>).
68      * Note : il serait possible aussi de le faire avec <a href='https://github.com/FasterXML/jackson'>Jackson</a>
69      */

70     JSON("application/json"),
71
72     /**
73      * GSON (écriture et lecture en JSON avec <a href='https://github.com/google/gson'>Google Gson</a>).
74      * Note : il serait possible aussi de le faire avec <a href='https://github.com/FasterXML/jackson'>Jackson</a>
75      */

76     GSON("application/json");
77
78     private static final String NULL_VALUE = "null";
79
80     // classe interne pour qu'elle ne soit pas chargée avec la classe TransportFormat
81     // et qu'ainsi on ne dépende pas de XStream si on ne se sert pas du format xml
82     private static final class XmlIO {
83         private static final String PACKAGE_NAME = TransportFormat.class.getName().substring(0,
84                 TransportFormat.class.getName().length()
85                         - TransportFormat.class.getSimpleName().length() - 1);
86         private static final String XML_CHARSET_NAME = "utf-8";
87
88         private XmlIO() {
89             super();
90         }
91
92         static void writeToXml(Serializable serializable, BufferedOutputStream bufferedOutput)
93                 throws IOException {
94             final XStream xstream = createXStream(false);
95             // on wrappe avec un CompactWriter pour gagner 25% en taille de flux (retours chariots)
96             // et donc un peu en performances
97             final CompactWriter writer = new CompactWriter(
98                     new OutputStreamWriter(bufferedOutput, XML_CHARSET_NAME));
99             try {
100                 xstream.marshal(serializable, writer);
101             } finally {
102                 writer.close();
103             }
104         }
105
106         static Object readFromXml(InputStream bufferedInput) throws IOException {
107             final XStream xstream = createXStream(false);
108             // see http://x-stream.github.io/security.html
109             // clear out existing permissions and set own ones
110             xstream.addPermission(NoTypePermission.NONE);
111             // allow some basics
112             xstream.addPermission(NullPermission.NULL);
113             xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
114             xstream.allowTypesByWildcard(
115                     new String[] { "java.lang.*""java.util.*""java.util.concurrent.*" });
116             // allow any type from the same package
117             xstream.allowTypesByWildcard(new String[] { PACKAGE_NAME + ".*" });
118             try (InputStreamReader reader = new InputStreamReader(bufferedInput,
119                     XML_CHARSET_NAME)) {
120                 return xstream.fromXML(reader);
121             }
122         }
123
124         static void writeToJson(Serializable serializable, BufferedOutputStream bufferedOutput)
125                 throws IOException {
126             final XStream xstream = createXStream(true);
127             try {
128                 xstream.toXML(serializable, bufferedOutput);
129             } finally {
130                 bufferedOutput.close();
131             }
132         }
133
134         private static XStream createXStream(boolean json) {
135             final XStream xstream;
136             if (json) {
137                 // format json
138                 xstream = new XStream(new JsonHierarchicalStreamDriver());
139                 // #884 removed xstream.setMode(XStream.NO_REFERENCES);
140             } else {
141                 // sinon format xml, utilise la dépendance XPP3 par défaut
142                 xstream = new XStream();
143             }
144             for (final Map.Entry<String, Class<?>> entry : XStreamAlias.getMap().entrySet()) {
145                 xstream.alias(entry.getKey(), entry.getValue());
146             }
147             final MapConverter mapConverter = new MapConverter(xstream.getMapper()) {
148                 /** {@inheritDoc} */
149                 @SuppressWarnings("rawtypes")
150                 @Override
151                 public boolean canConvert(Class type) {
152                     return true// Counter.requests est bien une map
153                 }
154             };
155             xstream.registerLocalConverter(Counter.class"requests", mapConverter);
156             xstream.registerLocalConverter(Counter.class"rootCurrentContextsByThreadId",
157                     mapConverter);
158             return xstream;
159         }
160     }
161
162     // classe interne pour qu'elle ne soit pas chargée avec la classe TransportFormat
163     // et qu'ainsi on ne dépende pas de GSON si on ne se sert pas du format gson
164     // ni de XStream si on ne se sert pas du format json
165     private static final class GsonIO {
166         private static final String GSON_CHARSET_NAME = "UTF-8";
167
168         private GsonIO() {
169             super();
170         }
171
172         static void writeToGson(Serializable serializable, BufferedOutputStream bufferedOutput)
173                 throws IOException {
174             final JsonSerializer<StackTraceElement> stackTraceElementJsonSerializer = new JsonSerializer<StackTraceElement>() {
175                 @Override
176                 public JsonElement serialize(StackTraceElement src, Type typeOfSrc,
177                         JsonSerializationContext context) {
178                     return new JsonPrimitive(src.toString());
179                 }
180             };
181             final Gson gson = new GsonBuilder()
182                     // .setPrettyPrinting() : prettyPrinting pas nécessaire avec un viewer de json
183                     .registerTypeAdapter(StackTraceElement.class, stackTraceElementJsonSerializer)
184                     .create();
185             try (OutputStreamWriter writer = new OutputStreamWriter(bufferedOutput,
186                     GSON_CHARSET_NAME)) {
187                 gson.toJson(serializable, writer);
188             }
189         }
190     }
191
192     private static class MyObjectInputStream extends ObjectInputStream {
193         private static final String PACKAGE_NAME = TransportFormat.class.getName().substring(0,
194                 TransportFormat.class.getName().length()
195                         - TransportFormat.class.getSimpleName().length() - 1);
196
197         MyObjectInputStream(InputStream input) throws IOException {
198             super(input);
199         }
200
201         // during deserialization, protect ourselves from malicious payload
202         // http://www.ibm.com/developerworks/library/se-lookahead/index.html
203         @Override
204         protected Class<?> resolveClass(ObjectStreamClass desc)
205                 throws IOException, ClassNotFoundException {
206             final String name = desc.getName();
207             int i = 0;
208             if (name.indexOf("[[") == 0) {
209                 // 2 dimensions array
210                 i++;
211             }
212             if (name.indexOf("[L", i) == i) {
213                 // 1 dimension array
214                 i += 2;
215             }
216             if (name.indexOf("java.lang.", i) == i || name.indexOf("java.util.", i) == i
217                     || name.indexOf("java.io.", i) == i || name.indexOf(PACKAGE_NAME, i) == i
218                     || name.length() <= 2) {
219                 // if name.length() == 2, primitive type or array (such as [B in javamelody-swing)
220                 return super.resolveClass(desc);
221             } else if (name.indexOf("net.bull.javamelody", i) == i) {
222                 // resolve old classes names to new classes names, for backward compatibility
223                 return Class.forName(
224                         name.replace("net.bull.javamelody""net.bull.javamelody.internal.model"));
225             }
226             throw new ClassNotFoundException(name);
227         }
228     }
229
230     private final String code; // NOPMD
231     private final String mimeType; // NOPMD
232
233     TransportFormat(String mimeType) {
234         this.mimeType = mimeType;
235         this.code = this.toString().toLowerCase(Locale.ENGLISH);
236     }
237
238     public static TransportFormat valueOfIgnoreCase(String transportFormat) {
239         return valueOf(transportFormat.toUpperCase(Locale.ENGLISH).trim());
240     }
241
242     public static boolean isATransportFormat(String format) {
243         if (format == null) {
244             return false;
245         }
246         final String upperCase = format.toUpperCase(Locale.ENGLISH).trim();
247         for (final TransportFormat transportFormat : values()) {
248             if (transportFormat.toString().equals(upperCase)) {
249                 return true;
250             }
251         }
252         return false;
253     }
254
255     public String getCode() {
256         return code;
257     }
258
259     public String getMimeType() {
260         return mimeType;
261     }
262
263     public void checkDependencies() throws IOException {
264         if (this == XML || this == JSON) {
265             try {
266                 Class.forName("com.thoughtworks.xstream.XStream");
267             } catch (final ClassNotFoundException e) {
268                 throw new IOException(
269                         "Classes of the XStream library not found. Add the XStream dependency in your webapp for the XML or JSON formats.",
270                         e);
271             }
272         }
273         if (this == XML) {
274             try {
275                 Class.forName("org.xmlpull.v1.XmlPullParser");
276             } catch (final ClassNotFoundException e) {
277                 throw new IOException(
278                         "Classes of the XPP3 library not found. Add the XPP3 dependency in your webapp for the XML format.",
279                         e);
280             }
281         }
282         if (this == GSON) {
283             try {
284                 Class.forName("com.google.gson.Gson");
285             } catch (final ClassNotFoundException e) {
286                 throw new IOException(
287                         "Classes of the Gson library not found. Add the Gson dependency in your webapp for the GSON format.",
288                         e);
289             }
290
291         }
292     }
293
294     public void writeSerializableTo(Serializable serializable, OutputStream output)
295             throws IOException {
296         final Serializable nonNullSerializable;
297         if (serializable == null) {
298             nonNullSerializable = NULL_VALUE;
299         } else {
300             nonNullSerializable = serializable;
301         }
302         final BufferedOutputStream bufferedOutput = new BufferedOutputStream(output);
303         switch (this) {
304         case SERIALIZED:
305             try (ObjectOutputStream out = new ObjectOutputStream(bufferedOutput)) {
306                 out.writeObject(nonNullSerializable);
307             }
308             break;
309         case XML:
310             // Rq : sans xstream et si jdk 1.6, on pourrait sinon utiliser
311             // XMLStreamWriter et XMLStreamReader ou JAXB avec des annotations
312             XmlIO.writeToXml(nonNullSerializable, bufferedOutput);
313             break;
314         case JSON:
315             XmlIO.writeToJson(nonNullSerializable, bufferedOutput);
316             break;
317         case GSON:
318             GsonIO.writeToGson(nonNullSerializable, bufferedOutput);
319             break;
320         default:
321             throw new IllegalStateException(toString());
322         }
323     }
324
325     Serializable readSerializableFrom(InputStream input)
326             throws IOException, ClassNotFoundException {
327         final InputStream bufferedInput = new BufferedInputStream(input);
328         final Object result;
329         switch (this) {
330         case SERIALIZED:
331             try (ObjectInputStream in = createObjectInputStream(bufferedInput)) {
332                 result = in.readObject();
333             }
334             break;
335         case XML:
336             result = XmlIO.readFromXml(bufferedInput);
337             break;
338         case JSON:
339             // pas possible avec JsonHierarchicalStreamDriver
340             // (http://x-stream.github.io/json-tutorial.html)
341             throw new UnsupportedOperationException();
342         case GSON:
343             throw new UnsupportedOperationException();
344         default:
345             throw new IllegalStateException(toString());
346         }
347         if (NULL_VALUE.equals(result)) {
348             return null;
349         }
350         // c'est un Serializable que l'on a écrit
351         return (Serializable) result;
352     }
353
354     static ObjectInputStream createObjectInputStream(InputStream input) throws IOException {
355         return new MyObjectInputStream(input);
356     }
357 }
358