001: /*
002: * All content copyright (c) 2003-2006 Terracotta, Inc., except as may otherwise be noted in a separate copyright
003: * notice. All rights reserved.
004: */
005: package com.tc.object.tools;
006:
007: import com.tc.logging.TCLogger;
008: import com.tc.logging.TCLogging;
009: import com.tc.object.NotInBootJar;
010: import com.tc.properties.TCProperties;
011: import com.tc.properties.TCPropertiesImpl;
012: import com.tc.util.Assert;
013: import com.tc.util.ProductInfo;
014:
015: import java.io.BufferedInputStream;
016: import java.io.ByteArrayOutputStream;
017: import java.io.File;
018: import java.io.FileNotFoundException;
019: import java.io.FileOutputStream;
020: import java.io.IOException;
021: import java.util.Enumeration;
022: import java.util.HashMap;
023: import java.util.HashSet;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.Set;
027: import java.util.StringTokenizer;
028: import java.util.jar.Attributes;
029: import java.util.jar.JarEntry;
030: import java.util.jar.JarFile;
031: import java.util.jar.JarOutputStream;
032: import java.util.jar.Manifest;
033:
034: public class BootJar {
035: private static final TCLogger logger = TCLogging
036: .getLogger(BootJar.class);
037:
038: private static final String DSO_BOOT_JAR_PATTERN = ".+dso-boot.*\\.jar$";
039: static final String JAR_NAME_PREFIX = "dso-boot-";
040:
041: private static final String VERSION_1_1 = "1.1";
042:
043: private static final State STATE_OPEN = new State("OPEN");
044: private static final State STATE_CLOSED = new State("CLOSED");
045:
046: public static final String PREINSTRUMENTED_NAME = "Preinstrumented";
047: public static final String FOREIGN_NAME = "Foreign";
048:
049: private final File file;
050: private final Map entries;
051: private final boolean openForWrite;
052: private final Manifest manifest;
053: private final BootJarMetaData metaData;
054: private final JarFile jarFileInput;
055: private final Set classes = new HashSet();
056: private State state;
057: private boolean creationErrorOccurred;
058:
059: private BootJar(File file, boolean openForWrite,
060: BootJarMetaData metaData, JarFile jarFile) {
061: Assert.assertNotNull(file);
062:
063: this .file = file;
064: this .openForWrite = openForWrite;
065: this .metaData = metaData;
066: this .jarFileInput = jarFile;
067: this .manifest = new Manifest();
068: this .entries = new HashMap();
069:
070: this .state = STATE_OPEN;
071: this .creationErrorOccurred = false;
072: }
073:
074: public static BootJar getBootJarForWriting(File bootJar)
075: throws UnsupportedVMException {
076: return getBootJarForWriting(bootJar, BootJarSignature
077: .getSignatureForThisVM().getSignature());
078: }
079:
080: public static BootJar getBootJarForReading(File bootJar)
081: throws IOException, BootJarException {
082: return getBootJarForReading(bootJar, BootJarSignature
083: .getSignatureForThisVM());
084: }
085:
086: static BootJar getBootJarForWriting(File bootJar, String vmSignature) {
087: return getBootJarForWriting(bootJar, vmSignature, VERSION_1_1);
088: }
089:
090: static BootJar getBootJarForWriting(File bootJar,
091: String vmSignature, String metaDataVersion) {
092: return new BootJar(bootJar, true, new BootJarMetaData(
093: vmSignature, metaDataVersion), null);
094: }
095:
096: static BootJar getBootJarForReading(File bootJar,
097: BootJarSignature expectedSignature) throws IOException,
098: BootJarException {
099: if (!existingFileIsAccessible(bootJar))
100: throw new FileNotFoundException("Cannot access file: "
101: + bootJar.getAbsolutePath());
102: JarFile jarFile = new JarFile(bootJar, false);
103: Manifest manifest = jarFile.getManifest();
104: BootJarMetaData metaData = new BootJarMetaData(manifest);
105:
106: // verify VM signature (iff l1.jvm.check.compatibility == true, see: tc.properties)
107: BootJarSignature signatureFromJar = new BootJarSignature(
108: metaData.getVMSignature());
109: TCProperties props = TCPropertiesImpl.getProperties()
110: .getPropertiesFor("l1");
111: final boolean checkJvmCompatibility = props
112: .getBoolean("jvm.check.compatibility");
113: if (checkJvmCompatibility
114: && !expectedSignature
115: .isCompatibleWith(signatureFromJar)) {
116: throw new InvalidJVMVersionException(
117: "Incompatible boot jar JVM version; expected '"
118: + expectedSignature
119: + "' but was (in boot jar) '"
120: + signatureFromJar
121: + "'; Please regenerate the DSO boot jar to match this VM, or switch to a VM compatible with this boot jar");
122: }
123: return new BootJar(bootJar, false, metaData, jarFile);
124: }
125:
126: public static File findBootJar() throws FileNotFoundException {
127: return findBootJarByPattern(DSO_BOOT_JAR_PATTERN);
128: }
129:
130: private static File findBootJarByPattern(String pattern)
131: throws FileNotFoundException {
132: String bootClassPath = System
133: .getProperty("sun.boot.class.path");
134: StringTokenizer st = new StringTokenizer(bootClassPath, System
135: .getProperty("path.separator"));
136: while (st.hasMoreTokens()) {
137: String element = st.nextToken();
138: if (element.matches(pattern)) {
139: return new File(element);
140: }
141: }
142: throw new FileNotFoundException(
143: "Can't find boot jar matching pattern (" + pattern
144: + ") in boot classpath: " + bootClassPath);
145: }
146:
147: public static BootJar getDefaultBootJarForReading()
148: throws BootJarException, IOException {
149: return getBootJarForReading(findBootJar());
150: }
151:
152: private static boolean existingFileIsAccessible(File file) {
153: return file.exists() || file.isFile() || file.canRead();
154: }
155:
156: public static String classNameToFileName(String className) {
157: return className.replace('.', '/') + ".class";
158: }
159:
160: public static String fileNameToClassName(String filename) {
161: if (!filename.endsWith(".class"))
162: throw new AssertionError("Invalid class file name: "
163: + filename);
164: return filename.substring(0, filename.lastIndexOf('.'))
165: .replace('/', '.');
166: }
167:
168: public boolean classLoaded(String className) {
169: return classes.contains(className);
170: }
171:
172: public void loadClassIntoJar(String className, byte[] data,
173: boolean isPreinstrumented) {
174: loadClassIntoJar(className, data, isPreinstrumented, false);
175: }
176:
177: public void loadClassIntoJar(String className, byte[] data,
178: boolean isPreinstrumented, boolean isForeign) {
179: boolean added = classes.add(className);
180:
181: // disallow duplicate entries into the boot jar. Even w/o this assertion, the jar
182: // stuff will blow up later if an entry is duplicated
183: Assert.assertTrue("Duplicate class added " + className, added);
184:
185: if (className.equals(NotInBootJar.class.getName())) {
186: // make formatter sane
187: throw new AssertionError("Invalid class for boot jar: "
188: + className);
189: }
190:
191: String cn = classNameToFileName(className);
192: JarEntry jarEntry = new JarEntry(cn);
193: basicLoadClassIntoJar(jarEntry, data, isPreinstrumented,
194: isForeign);
195: }
196:
197: private void assertWrite() {
198: if (!openForWrite)
199: throw new AssertionError("boot jar not open for writing");
200: }
201:
202: private synchronized void basicLoadClassIntoJar(JarEntry je,
203: byte[] classBytes, boolean isPreinstrumented,
204: boolean isForeign) {
205: assertWrite();
206:
207: Attributes attributes = manifest.getAttributes(je.getName());
208: if (attributes == null) {
209: attributes = makeAttributesFor(je.getName());
210: }
211: attributes.put(new Attributes.Name(PREINSTRUMENTED_NAME),
212: Boolean.toString(isPreinstrumented));
213: attributes.put(new Attributes.Name(FOREIGN_NAME), Boolean
214: .toString(isForeign));
215: entries.put(
216: new JarEntryWrapper(je, Boolean
217: .valueOf(isPreinstrumented), Boolean
218: .valueOf(isForeign)), classBytes);
219: }
220:
221: private Attributes makeAttributesFor(String resource) {
222: Attributes rv = new Attributes();
223: manifest.getEntries().put(resource, rv);
224: return rv;
225: }
226:
227: private static final int QUERY_ALL = 0;
228: private static final int QUERY_PREINSTRUMENTED = 1;
229: private static final int QUERY_UNINSTRUMENTED = 2;
230: private static final int QUERY_FOREIGN = 3;
231:
232: public synchronized byte[] getBytesForClass(final String name)
233: throws IOException {
234: JarEntry entry = jarFileInput.getJarEntry(BootJar
235: .classNameToFileName(name));
236: final int bufsize = 4098;
237: final byte[] buffer = new byte[bufsize];
238: final BufferedInputStream is = new BufferedInputStream(
239: jarFileInput.getInputStream(entry));
240: ByteArrayOutputStream os = new ByteArrayOutputStream();
241: for (int k = 0; (k = is.read(buffer)) != -1;) {
242: os.write(buffer, 0, k);
243: }
244: is.close();
245: return os.toByteArray();
246: }
247:
248: private synchronized Set getBootJarClassNames(int query)
249: throws IOException {
250: assertOpen();
251: Set rv = new HashSet();
252: for (Enumeration e = jarFileInput.entries(); e
253: .hasMoreElements();) {
254: JarEntry entry = (JarEntry) e.nextElement();
255: String entryName = entry.getName();
256:
257: // This condition used to only exclude "META-INF/MANIFEST.MF". Jar signing puts additional META-INF files into the
258: // boot jar, which caused an assertion error. So, now only try reading specs from actual class files present in
259: // the jar
260: if (entryName.toLowerCase().endsWith(".class")) {
261: switch (query) {
262: case QUERY_ALL:
263: rv.add(fileNameToClassName(entry.getName()));
264: break;
265: case QUERY_PREINSTRUMENTED:
266: if (isPreInstrumentedEntry(entry)) {
267: rv.add(fileNameToClassName(entry.getName()));
268: }
269: break;
270: case QUERY_UNINSTRUMENTED:
271: if (!isPreInstrumentedEntry(entry)) {
272: rv.add(fileNameToClassName(entry.getName()));
273: }
274: break;
275: case QUERY_FOREIGN:
276: if (isForeign(entry)) {
277: rv.add(fileNameToClassName(entry.getName()));
278: }
279: default:
280: Assert
281: .failure("Query arg for getBootJarClasses() must be one of the following: QUERY_ALL, QUERY_PREINSTRUMENTED, QUERY_UNINSTRUMENTED, QUERY_FOREIGN");
282: break;
283: }
284: }
285: }
286: return rv;
287: }
288:
289: public synchronized Set getAllClasses() throws IOException {
290: return getBootJarClassNames(QUERY_ALL);
291: }
292:
293: public synchronized Set getAllUninstrumentedClasses()
294: throws IOException {
295: return getBootJarClassNames(QUERY_UNINSTRUMENTED);
296: }
297:
298: public synchronized Set getAllPreInstrumentedClasses()
299: throws IOException {
300: return getBootJarClassNames(QUERY_PREINSTRUMENTED);
301: }
302:
303: public synchronized Set getAllForeignClasses() throws IOException {
304: return getBootJarClassNames(QUERY_FOREIGN);
305: }
306:
307: private void assertOpen() {
308: if (state != STATE_OPEN) {
309: throw new AssertionError("boot jar not open: " + state);
310: }
311: }
312:
313: private String getJarEntryAttributeValue(JarEntry entry,
314: String attributeName) throws IOException {
315: Attributes attributes = entry.getAttributes();
316: if (attributes == null)
317: throw new AssertionError(
318: "Invalid jar file: No attributes for jar entry: "
319: + entry.getName());
320: String value = attributes.getValue(attributeName);
321: if (value == null)
322: throw new AssertionError("Invalid jar file: No "
323: + attributeName + " attribute for jar entry: "
324: + entry.getName());
325: return value;
326:
327: }
328:
329: private boolean isForeign(JarEntry entry) throws IOException {
330: return Boolean.valueOf(
331: getJarEntryAttributeValue(entry, FOREIGN_NAME))
332: .booleanValue();
333: }
334:
335: private boolean isPreInstrumentedEntry(JarEntry entry)
336: throws IOException {
337: return Boolean.valueOf(
338: getJarEntryAttributeValue(entry, PREINSTRUMENTED_NAME))
339: .booleanValue();
340: }
341:
342: private void writeEntries(JarOutputStream out) throws IOException {
343: for (Iterator i = entries.keySet().iterator(); i.hasNext();) {
344: JarEntryWrapper je = (JarEntryWrapper) i.next();
345: byte[] classBytes = (byte[]) entries.get(je);
346: out.putNextEntry(je.getJarEntry());
347: out.write(classBytes);
348: }
349: }
350:
351: public void setCreationErrorOccurred(boolean errorOccurred) {
352: this .creationErrorOccurred = errorOccurred;
353: }
354:
355: public synchronized void close() throws IOException {
356: if (state == STATE_OPEN) {
357: if (openForWrite && !this .creationErrorOccurred) {
358: metaData.write(manifest);
359: JarOutputStream out = new JarOutputStream(
360: new FileOutputStream(file, false), manifest);
361: writeEntries(out);
362: out.flush();
363: out.close();
364: }
365:
366: if (jarFileInput != null) {
367: jarFileInput.close();
368: }
369: }
370:
371: state = STATE_CLOSED;
372: }
373:
374: /**
375: * Check the embedded TC_VERSION information against current product version.
376: *
377: * @return <code>true</code> TC_VERSION matches current product version.
378: */
379: public boolean checkSourceVersion() {
380: return false;
381: }
382:
383: static class BootJarMetaData {
384: private static final String META_DATA_ATTRIBUTE_NAME = "DSO_BOOTJAR_METADATA";
385: private static final String TC_VERSION = "TC_VERSION";
386: private static final String TC_MONIKER = "TC_MONIKER";
387: private static final String VERSION = "VERSION";
388: private static final String VM_SIGNATURE = "VM_SIGNATURE";
389:
390: private final String vmSignature;
391: private final String version;
392: private final String tcversion;
393: private final String tcmoniker;
394:
395: BootJarMetaData(String vmSignature, String version) {
396: Assert.assertNotNull(vmSignature);
397: Assert.assertNotNull(version);
398: this .vmSignature = vmSignature;
399: this .version = version;
400: this .tcversion = null;
401: this .tcmoniker = null;
402: }
403:
404: BootJarMetaData(Manifest manifest) throws BootJarException {
405: Assert.assertNotNull(manifest);
406: Attributes attributes = (Attributes) manifest.getEntries()
407: .get(META_DATA_ATTRIBUTE_NAME);
408: if (attributes == null) {
409: throw new InvalidBootJarMetaDataException(
410: "Missing attributes in jar manifest.");
411: }
412:
413: version = attributes.getValue(VERSION);
414: if (version == null) {
415: throw new InvalidBootJarMetaDataException(
416: "Missing metadata: version.");
417: }
418:
419: String expect_version = VERSION_1_1;
420: if (expect_version.equals(version)) {
421: vmSignature = attributes.getValue(VM_SIGNATURE);
422: if (vmSignature == null) {
423: throw new InvalidJVMVersionException(
424: "Missing vm signature.");
425: }
426: } else {
427: throw new InvalidBootJarMetaDataException(
428: "Incompatible DSO meta data: version; expected '"
429: + expect_version
430: + "' but was (in boot jar): '"
431: + version);
432: }
433:
434: tcversion = attributes.getValue(TC_VERSION);
435: if (tcversion == null) {
436: throw new InvalidBootJarMetaDataException(
437: "Missing metadata: tcversion.");
438: }
439:
440: tcmoniker = attributes.getValue(TC_MONIKER);
441: if (tcmoniker == null) {
442: throw new InvalidBootJarMetaDataException(
443: "Missing metadata: tcmoniker.");
444: }
445:
446: ProductInfo productInfo = ProductInfo.getInstance();
447: String expect_tcversion = productInfo.buildVersion();
448:
449: if (productInfo.isDevMode()) {
450: logger
451: .warn("The value for the DSO meta data, tcversion is: '"
452: + expect_tcversion
453: + "'; this might not be correct, this value is used only under development mode or when tests are being run.");
454: }
455: if (!productInfo.isDevMode()
456: && !expect_tcversion.equals(tcversion)) {
457: throw new InvalidBootJarMetaDataException(
458: "Incompatible DSO meta data: tcversion; expected '"
459: + expect_tcversion
460: + "' but was (in boot jar): '"
461: + tcversion + "'");
462: }
463: }
464:
465: public void write(Manifest manifest) {
466: if (VERSION_1_1.equals(version)) {
467: ProductInfo productInfo = ProductInfo.getInstance();
468: Attributes attributes = new Attributes();
469: attributes.put(new Attributes.Name(TC_MONIKER),
470: productInfo.moniker());
471: attributes.put(new Attributes.Name(TC_VERSION),
472: productInfo.buildVersion());
473: attributes.put(new Attributes.Name(VERSION),
474: getVersion());
475: attributes.put(new Attributes.Name(VM_SIGNATURE),
476: getVMSignature());
477: Object prev = manifest.getEntries().put(
478: META_DATA_ATTRIBUTE_NAME, attributes);
479: Assert.assertNull(prev);
480: } else {
481: throw new AssertionError(
482: "Unexptected metadata for version, expecting '"
483: + VERSION_1_1 + "', but was '"
484: + version + "'");
485: }
486: }
487:
488: public String getTCVersion() {
489: return this .tcversion;
490: }
491:
492: public String getVMSignature() {
493: return this .vmSignature;
494: }
495:
496: public String getVersion() {
497: return this .version;
498: }
499: }
500:
501: private static class JarEntryWrapper {
502: private final JarEntry jarEntry;
503: private final Boolean isPreinstrumented;
504: private final Boolean isForeign;
505:
506: private JarEntryWrapper(JarEntry jarEntry,
507: Boolean isPreinstrumented, Boolean isForeign) {
508: this .jarEntry = jarEntry;
509: this .isPreinstrumented = isPreinstrumented;
510: this .isForeign = isForeign;
511: }
512:
513: public JarEntry getJarEntry() {
514: return jarEntry;
515: }
516:
517: public Boolean isPreinstrumented() {
518: return isPreinstrumented;
519: }
520:
521: public Boolean isForeign() {
522: return isForeign;
523: }
524: }
525:
526: private static class State {
527: private final String name;
528:
529: private State(String name) {
530: this .name = name;
531: }
532:
533: public String toString() {
534: return this.name;
535: }
536: }
537: }
|