1 /*-
2  * See the file LICENSE for redistribution information.
3  *
4  * Copyright (c) 2002, 2013 Oracle and/or its affiliates.  All rights reserved.
5  *
6  */
7 
8 package com.sleepycat.persist.model;
9 
10 import java.io.File;
11 import java.io.FileInputStream;
12 import java.io.FileOutputStream;
13 import java.io.IOException;
14 import java.lang.instrument.ClassFileTransformer;
15 import java.lang.instrument.Instrumentation;
16 import java.security.ProtectionDomain;
17 import java.util.ArrayList;
18 import java.util.HashSet;
19 import java.util.List;
20 import java.util.Set;
21 import java.util.StringTokenizer;
22 
23 import com.sleepycat.asm.ClassReader;
24 import com.sleepycat.asm.ClassVisitor;
25 import com.sleepycat.asm.ClassWriter;
26 
27 /**
28  * Enhances the bytecode of persistent classes to provide efficient access to
29  * fields and constructors, and to avoid special security policy settings for
30  * accessing non-public members.  Classes are enhanced if they are annotated
31  * with {@link Entity} or {@link Persistent}.
32  *
33  * <p>{@code ClassEnhancer} objects are thread-safe.  Multiple threads may
34  * safely call the methods of a shared {@code ClassEnhancer} object.</p>
35  *
36  * <p>As described in the {@link <a
37  * href="../package-summary.html#bytecode">package summary</a>}, bytecode
38  * enhancement may be used either at runtime or offline (at build time).</p>
39  *
40  * <p>To use enhancement offline, this class may be used as a {@link #main main
41  * program}.
42  * </p>
43  *
44  * <p>For enhancement at runtime, this class provides the low level support
45  * needed to transform class bytes during class loading.  To configure runtime
46  * enhancement you may use one of the following approaches:</p>
47  * <ol>
48  * <li>The BDB {@code je-<version>.jar} or {@code db.jar} file may be used as
49  * an instrumentation agent as follows:
50  * <pre class="code">{@literal java -javaagent:<BDB-JAR-FILE>=enhance:packageNames ...}</pre>
51  * {@code packageNames} is a comma separated list of packages containing
52  * persistent classes.  Sub-packages of these packages are also searched.  If
53  * {@code packageNames} is omitted then all packages known to the current
54  * classloader are searched.
55  * <p>The "-v" option may be included in the comma separated list to print the
56  * name of each class that is enhanced.</p></li>
57  * <br>
58  * <li>The {@link #enhance} method may be called to implement a class loader
59  * that performs enhancement.  Using this approach, it is the developer's
60  * responsibility to implement and configure the class loader.</li>
61  * </ol>
62  *
63  * @author Mark Hayes
64  */
65 public class ClassEnhancer implements ClassFileTransformer {
66 
67     private static final String AGENT_PREFIX = "enhance:";
68 
69     private Set<String> packagePrefixes;
70     private boolean verbose;
71 
72     /**
73      * Enhances classes in the directories specified.  The class files are
74      * replaced when they are enhanced, without changing the file modification
75      * date.  For example:
76      *
77      * <pre class="code">java -cp je-&lt;version&gt;.jar com.sleepycat.persist.model.ClassEnhancer ./classes</pre>
78      *
79      * <p>The "-v" argument may be specified to print the name of each class
80      * file that is enhanced.  The total number of class files enhanced will
81      * always be printed.</p>
82      *
83      * @param args one or more directories containing classes to be enhanced.
84      * Subdirectories of these directories will also be searched.  Optionally,
85      * -v may be included to print the name of every class file enhanced.
86      */
main(String[] args)87     public static void main(String[] args) throws Exception {
88         try {
89             boolean verbose = false;
90             List<File> fileList = new ArrayList<File>();
91             for (int i = 0; i < args.length; i += 1) {
92                 String arg = args[i];
93                 if (arg.startsWith("-")) {
94                     if ("-v".equals(args[i])) {
95                         verbose = true;
96                     } else {
97                         throw new IllegalArgumentException
98                             ("Unknown arg: " + arg);
99                     }
100                 } else {
101                     fileList.add(new File(arg));
102                 }
103             }
104             ClassEnhancer enhancer = new ClassEnhancer();
105             enhancer.setVerbose(verbose);
106             int nFiles = 0;
107             for (File file : fileList) {
108                 nFiles += enhancer.enhanceFile(file);
109             }
110             if (nFiles > 0) {
111                 System.out.println("Enhanced: " + nFiles + " files");
112             }
113         } catch (Exception e) {
114             e.printStackTrace();
115             throw e;
116         }
117     }
118 
119     /**
120      * Enhances classes as specified by a JVM -javaagent argument.
121      *
122      * @see java.lang.instrument.Instrumentation
123      */
premain(String args, Instrumentation inst)124     public static void premain(String args, Instrumentation inst) {
125         if (!args.startsWith(AGENT_PREFIX)) {
126             throw new IllegalArgumentException
127                 ("Unknown javaagent args: " + args +
128                  " Args must start with: \"" + AGENT_PREFIX + '"');
129         }
130         args = args.substring(AGENT_PREFIX.length());
131         Set<String> packageNames = null;
132         boolean verbose = false;
133         if (args.length() > 0) {
134             packageNames = new HashSet<String>();
135             StringTokenizer tokens = new StringTokenizer(args, ",");
136             while (tokens.hasMoreTokens()) {
137                 String token = tokens.nextToken();
138                 if (token.startsWith("-")) {
139                     if (token.equals("-v")) {
140                         verbose = true;
141                     } else {
142                         throw new IllegalArgumentException
143                             ("Unknown javaagent arg: " + token);
144                     }
145                 } else {
146                     packageNames.add(token);
147                 }
148             }
149         }
150         ClassEnhancer enhancer = new ClassEnhancer(packageNames);
151         enhancer.setVerbose(verbose);
152         inst.addTransformer(enhancer);
153     }
154 
155     /**
156      * Creates a class enhancer that searches all packages.
157      */
ClassEnhancer()158     public ClassEnhancer() {
159     }
160 
161     /**
162      * Sets verbose mode.
163      *
164      * <p>True may be specified to print the name of each class file that is
165      * enhanced.  This property is false by default.</p>
166      */
setVerbose(boolean verbose)167     public void setVerbose(boolean verbose) {
168         this.verbose = verbose;
169     }
170 
171     /**
172      * Gets verbose mode.
173      *
174      * @see #setVerbose
175      */
getVerbose()176     public boolean getVerbose() {
177         return verbose;
178     }
179 
180     /**
181      * Creates a class enhancer that searches a given set of packages.
182      *
183      * @param packageNames a set of packages to search for persistent
184      * classes.  Sub-packages of these packages are also searched.  If empty or
185      * null, all packages known to the current classloader are searched.
186      */
ClassEnhancer(Set<String> packageNames)187     public ClassEnhancer(Set<String> packageNames) {
188         if (packageNames != null) {
189             packagePrefixes = new HashSet<String>();
190             for (String name : packageNames) {
191                 packagePrefixes.add(name + '.');
192             }
193         }
194     }
195 
transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)196     public byte[] transform(ClassLoader loader,
197                             String className,
198                             Class<?> classBeingRedefined,
199                             ProtectionDomain protectionDomain,
200                             byte[] classfileBuffer) {
201         className = className.replace('/', '.');
202         byte[] bytes = enhance(className, classfileBuffer);
203         if (verbose && bytes != null) {
204             System.out.println("Enhanced: " + className);
205         }
206         return bytes;
207     }
208 
209     /**
210      * Enhances the given class bytes if the class is annotated with {@link
211      * Entity} or {@link Persistent}.
212      *
213      * @param className the class name in binary format; for example,
214      * "my.package.MyClass$Name", or null if no filtering by class name
215      * should be performed.
216      *
217      * @param classBytes are the class file bytes to be enhanced.
218      *
219      * @return the enhanced bytes, or null if no enhancement was performed.
220      */
enhance(String className, byte[] classBytes)221     public byte[] enhance(String className, byte[] classBytes) {
222         if (className != null && packagePrefixes != null) {
223             for (String prefix : packagePrefixes) {
224                 if (className.startsWith(prefix)) {
225                     return enhanceBytes(classBytes);
226                 }
227             }
228             return null;
229         } else {
230             return enhanceBytes(classBytes);
231         }
232     }
233 
enhanceFile(File file)234     int enhanceFile(File file)
235         throws IOException {
236 
237         int nFiles = 0;
238         if (file.isDirectory()) {
239             String[] names = file.list();
240             if (names != null) {
241                 for (int i = 0; i < names.length; i += 1) {
242                     nFiles += enhanceFile(new File(file, names[i]));
243                 }
244             }
245         } else if (file.getName().endsWith(".class")) {
246             byte[] newBytes = enhanceBytes(readFile(file));
247             if (newBytes != null) {
248                 long modified = file.lastModified();
249                 writeFile(file, newBytes);
250                 file.setLastModified(modified);
251                 nFiles += 1;
252                 if (verbose) {
253                     System.out.println("Enhanced: " + file);
254                 }
255             }
256         }
257         return nFiles;
258     }
259 
readFile(File file)260     private byte[] readFile(File file)
261         throws IOException {
262 
263         byte[] bytes = new byte[(int) file.length()];
264         FileInputStream in = new FileInputStream(file);
265         try {
266             in.read(bytes);
267         } finally {
268             in.close();
269         }
270         return bytes;
271     }
272 
writeFile(File file, byte[] bytes)273     private void writeFile(File file, byte[] bytes)
274         throws IOException {
275 
276         FileOutputStream out = new FileOutputStream(file);
277         try {
278             out.write(bytes);
279         } finally {
280             out.close();
281         }
282     }
283 
enhanceBytes(byte[] bytes)284     private byte[] enhanceBytes(byte[] bytes) {
285 
286         /*
287          * The writer is at the end of the visitor chain.  Pass COMPUTE_FRAMES
288          * to calculate stack size, for safety.
289          */
290         ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
291         ClassVisitor visitor = writer;
292 
293         /* The enhancer is at the beginning of the visitor chain. */
294         visitor = new BytecodeEnhancer(visitor);
295 
296         /* The reader processes the class and invokes the visitors. */
297         ClassReader reader = new ClassReader(bytes);
298         try {
299 
300             /*
301              * Pass false for skipDebug since we are rewriting the class and
302              * should include all information.
303              */
304             reader.accept(visitor, 0);
305             return writer.toByteArray();
306         } catch (BytecodeEnhancer.NotPersistentException e) {
307             /* The class is not persistent and should not be enhanced. */
308             return null;
309         }
310     }
311 }
312