View Javadoc

1   /*
2    * Grapht, an open source dependency injector.
3    * Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt)
4    * Copyright 2010-2014 Regents of the University of Minnesota
5    *
6    * This program is free software; you can redistribute it and/or modify
7    * it under the terms of the GNU Lesser General Public License as
8    * published by the Free Software Foundation; either version 2.1 of the
9    * License, or (at your option) any later version.
10   *
11   * This program is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13   * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14   * details.
15   *
16   * You should have received a copy of the GNU General Public License along with
17   * this program; if not, write to the Free Software Foundation, Inc., 51
18   * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19   */
20  package org.grouplens.grapht.util;
21  
22  import org.apache.commons.lang3.ClassUtils;
23  import org.apache.commons.lang3.StringUtils;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import javax.annotation.Nullable;
28  import javax.annotation.concurrent.Immutable;
29  import javax.inject.Inject;
30  import java.io.IOException;
31  import java.io.ObjectInputStream;
32  import java.io.Serializable;
33  import java.lang.ref.WeakReference;
34  import java.lang.reflect.*;
35  import java.nio.ByteBuffer;
36  import java.nio.charset.Charset;
37  import java.security.MessageDigest;
38  import java.security.NoSuchAlgorithmException;
39  import java.util.*;
40  
41  /**
42   * A serialization proxy for class instances.  This serializable class encapsulates a simple
43   * representation for classes when serializing object graphs.
44   * <p>
45   *     When using this class, classes are serialized as their binary name, as returned by
46   *     {@link Class#getName()}.  The name encodes array information, so this is adequate
47   *     to fully reconstruct the class.
48   * </p>
49   *
50   * @author <a href="http://grouplens.org">GroupLens Research</a>
51   */
52  @Immutable
53  public final class ClassProxy implements Serializable {
54      private static final long serialVersionUID = 1;
55      private static final Logger logger = LoggerFactory.getLogger(ClassProxy.class);
56  
57      private final String className;
58      private final long checksum;
59      @Nullable
60      private transient volatile WeakReference<Class<?>> theClass;
61      private transient ClassLoader classLoader;
62  
63      private ClassProxy(String name, long check) {
64          className = name;
65          checksum = check;
66          classLoader = ClassLoaders.inferDefault(ClassProxy.class);
67      }
68  
69      private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
70          stream.defaultReadObject();
71          classLoader = ClassLoaders.inferDefault(ClassProxy.class);
72      }
73  
74      /**
75       * Get the class name. This name does not include any array information.
76       * @return The class name.
77       */
78      public String getClassName() {
79          return className;
80      }
81  
82      @Override
83      public String toString() {
84          return "proxy of " + className;
85      }
86  
87      @Override
88      public boolean equals(Object o) {
89          if (o == this) {
90              return true;
91          } else if (o instanceof ClassProxy) {
92              ClassProxy op = (ClassProxy) o;
93              return className.equals(op.className);
94          } else {
95              return false;
96          }
97      }
98  
99      @Override
100     public int hashCode() {
101         return className.hashCode();
102     }
103 
104     /**
105      * Resolve a class proxy to a class.
106      * @return The class represented by this proxy.
107      * @throws ClassNotFoundException if the class represented by this proxy cannot be found.
108      */
109     public Class<?> resolve() throws ClassNotFoundException {
110         WeakReference<Class<?>> ref = theClass;
111         Class<?> cls = ref == null ? null : ref.get();
112         if (cls == null) {
113             if(className.equals("void")) {
114                 // special case
115                 cls = Void.TYPE;
116             } else {
117                 cls = ClassUtils.getClass(classLoader, className);
118             }
119             long check = checksumClass(cls);
120             if (!isSerializationPermissive() && checksum != check) {
121                 throw new ClassNotFoundException("checksum mismatch for " + cls.getName());
122             } else {
123                 if (checksum != check) {
124                     logger.warn("checksum mismatch for {}", cls);
125                 }
126                 theClass = new WeakReference<Class<?>>(cls);
127             }
128         }
129         return cls;
130     }
131 
132     private static final Map<Class<?>, ClassProxy> proxyCache = new WeakHashMap<Class<?>, ClassProxy>();
133 
134     /**
135      * Construct a class proxy for a class.
136      *
137      * @param cls The class.
138      * @return The class proxy.
139      */
140     public static synchronized ClassProxy of(Class<?> cls) {
141         ClassProxy proxy = proxyCache.get(cls);
142         if (proxy == null) {
143             proxy = new ClassProxy(cls.getName(), checksumClass(cls));
144             proxy.theClass = new WeakReference<Class<?>>(cls);
145             proxyCache.put(cls, proxy);
146         }
147         return proxy;
148     }
149 
150     private static final Charset UTF8 = Charset.forName("UTF-8");
151 
152     public static boolean isSerializationPermissive() {
153         return Boolean.getBoolean("grapht.deserialization.permissive");
154     }
155 
156     /**
157      * Compute a checksum for a class. These checksums are used to see if a class has changed
158      * its definition since being serialized.
159      * <p>
160      * The checksum used here is not cryptographically strong. It is intended only as a sanity
161      * check to detect incompatible serialization, not to robustly prevent tampering. The
162      * checksum algorithm currently is to compute an MD5 checksum over class member signatures
163      * and XOR the lower and upper halves of the checksum.
164      * </p>
165      *
166      * @param type The class to checksum.
167      * @return The
168      */
169     private static long checksumClass(Class<?> type) {
170         MessageDigest digest;
171         try {
172             digest = MessageDigest.getInstance("MD5");
173         } catch (NoSuchAlgorithmException e) {
174             throw new RuntimeException("JVM does not support MD5", e);
175         }
176         checksumClass(type, digest);
177 
178         ByteBuffer buf = ByteBuffer.wrap(digest.digest());
179         long l1 = buf.getLong();
180         long l2 = buf.getLong();
181         return l1 ^ l2;
182     }
183 
184     private static void checksumClass(Class<?> type, MessageDigest digest) {
185         // we compute a big hash of all the members of the class, and its superclasses.
186 
187         List<String> members = new ArrayList<String>();
188         for (Constructor<?> c: type.getDeclaredConstructors()) {
189             if (isInjectionSensitive(c)) {
190                 members.add(String.format("%s(%s)", c.getName(),
191                                           StringUtils.join(c.getParameterTypes(), ", ")));
192             }
193         }
194         for (Method m: type.getDeclaredMethods()) {
195             if (isInjectionSensitive(m)) {
196                 members.add(String.format("%s(%s): %s", m.getName(),
197                                           StringUtils.join(m.getParameterTypes(), ", "),
198                                           m.getReturnType()));
199             }
200         }
201         for (Field f: type.getDeclaredFields()) {
202             if (isInjectionSensitive(f)) {
203                 members.add(f.getName() + ":" + f.getType().getName());
204             }
205         }
206 
207         Collections.sort(members);
208 
209         Class<?> sup = type.getSuperclass();
210         if (sup != null) {
211             checksumClass(sup, digest);
212         }
213         for (String mem: members) {
214             digest.update(mem.getBytes(UTF8));
215         }
216     }
217 
218     /**
219      * Check whether a member is injection-sensitive and should be checked for validity in
220      * deserialization.
221      *
222      * @param m The member.
223      * @param <M> The type of member (done so we can check multiple types).
224      * @return {@code true} if the member should be checksummed, {@code false} to ignore it.
225      */
226     private static <M extends Member & AnnotatedElement>boolean isInjectionSensitive(M m) {
227         // static methods are not sensitive
228         if (Modifier.isStatic(m.getModifiers())) {
229             return false;
230         }
231 
232         // private members w/o @Inject are not sensitive
233         if (Modifier.isPrivate(m.getModifiers()) && m.getAnnotation(Inject.class) == null) {
234             return false;
235         }
236 
237         // public, protected, or @Inject - it's sensitive (be conservative)
238         return true;
239     }
240 }