View Javadoc
1   /*
2    * Copyright (C) 2007-2012 Argeo GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.argeo.slc.repo;
17  
18  import java.io.ByteArrayInputStream;
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.List;
25  import java.util.Properties;
26  import java.util.jar.Attributes;
27  import java.util.jar.Attributes.Name;
28  import java.util.jar.JarEntry;
29  import java.util.jar.JarInputStream;
30  import java.util.jar.Manifest;
31  
32  import javax.jcr.Binary;
33  import javax.jcr.Node;
34  import javax.jcr.NodeIterator;
35  import javax.jcr.Property;
36  import javax.jcr.RepositoryException;
37  import javax.jcr.Session;
38  import javax.jcr.nodetype.NodeType;
39  
40  import org.apache.commons.io.FilenameUtils;
41  import org.apache.commons.io.IOUtils;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.argeo.jcr.JcrUtils;
45  import org.argeo.slc.SlcException;
46  import org.argeo.slc.SlcNames;
47  import org.argeo.slc.SlcTypes;
48  import org.osgi.framework.Constants;
49  import org.osgi.framework.Version;
50  
51  /**
52   * Indexes jar file, currently supports standard J2SE and OSGi metadata (both
53   * from MANIFEST)
54   */
55  public class JarFileIndexer implements NodeIndexer, SlcNames {
56  	private final static Log log = LogFactory.getLog(JarFileIndexer.class);
57  	private Boolean force = false;
58  
59  	public Boolean support(String path) {
60  		return FilenameUtils.getExtension(path).equals("jar");
61  	}
62  
63  	public void index(Node fileNode) {
64  		Binary fileBinary = null;
65  		JarInputStream jarIn = null;
66  		ByteArrayOutputStream bo = null;
67  		ByteArrayInputStream bi = null;
68  		Binary manifestBinary = null;
69  		try {
70  			if (!support(fileNode.getPath()))
71  				return;
72  
73  			// Already indexed
74  			if (!force && fileNode.isNodeType(SlcTypes.SLC_JAR_FILE))
75  				return;
76  
77  			if (!fileNode.isNodeType(NodeType.NT_FILE))
78  				return;
79  
80  			Session jcrSession = fileNode.getSession();
81  			Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
82  			fileBinary = contentNode.getProperty(Property.JCR_DATA).getBinary();
83  
84  			jarIn = new JarInputStream(fileBinary.getStream());
85  			Manifest manifest = jarIn.getManifest();
86  			if (manifest == null) {
87  				log.error(fileNode + " has no MANIFEST");
88  				return;
89  			}
90  
91  			bo = new ByteArrayOutputStream();
92  			manifest.write(bo);
93  			byte[] newManifest = bo.toByteArray();
94  			if (fileNode.hasProperty(SLC_MANIFEST)) {
95  				byte[] storedManifest = JcrUtils.getBinaryAsBytes(fileNode
96  						.getProperty(SLC_MANIFEST));
97  				if (Arrays.equals(newManifest, storedManifest)) {
98  					if (log.isTraceEnabled())
99  						log.trace("Manifest not changed, doing nothing "
100 								+ fileNode);
101 					return;
102 				}
103 			}
104 
105 			bi = new ByteArrayInputStream(newManifest);
106 			manifestBinary = jcrSession.getValueFactory().createBinary(bi);
107 
108 			// standard jar file
109 			fileNode.addMixin(SlcTypes.SLC_JAR_FILE);
110 
111 			fileNode.setProperty(SlcNames.SLC_MANIFEST, manifestBinary);
112 			Attributes attrs = manifest.getMainAttributes();
113 
114 			getI18nValues(fileBinary, attrs);
115 
116 			// standard J2SE MANIFEST attributes
117 			addAttr(Attributes.Name.MANIFEST_VERSION, fileNode, attrs);
118 			addAttr(Attributes.Name.SIGNATURE_VERSION, fileNode, attrs);
119 			addAttr(Attributes.Name.CLASS_PATH, fileNode, attrs);
120 			addAttr(Attributes.Name.MAIN_CLASS, fileNode, attrs);
121 			addAttr(Attributes.Name.EXTENSION_NAME, fileNode, attrs);
122 			addAttr(Attributes.Name.IMPLEMENTATION_VERSION, fileNode, attrs);
123 			addAttr(Attributes.Name.IMPLEMENTATION_VENDOR, fileNode, attrs);
124 			addAttr(Attributes.Name.IMPLEMENTATION_VENDOR_ID, fileNode, attrs);
125 			addAttr(Attributes.Name.SPECIFICATION_TITLE, fileNode, attrs);
126 			addAttr(Attributes.Name.SPECIFICATION_VERSION, fileNode, attrs);
127 			addAttr(Attributes.Name.SPECIFICATION_VENDOR, fileNode, attrs);
128 			addAttr(Attributes.Name.SEALED, fileNode, attrs);
129 
130 			// OSGi
131 			if (attrs.containsKey(new Name(Constants.BUNDLE_SYMBOLICNAME))) {
132 				addOsgiMetadata(fileNode, attrs);
133 				if (log.isTraceEnabled())
134 					log.trace("Indexed OSGi bundle " + fileNode);
135 			} else {
136 				if (log.isTraceEnabled())
137 					log.trace("Indexed JAR file " + fileNode);
138 			}
139 
140 			JcrUtils.updateLastModified(fileNode);
141 
142 		} catch (Exception e) {
143 			throw new SlcException("Cannot index jar " + fileNode, e);
144 		} finally {
145 			IOUtils.closeQuietly(bi);
146 			IOUtils.closeQuietly(bo);
147 			IOUtils.closeQuietly(jarIn);
148 			JcrUtils.closeQuietly(manifestBinary);
149 			JcrUtils.closeQuietly(fileBinary);
150 		}
151 
152 	}
153 
154 	private void getI18nValues(Binary fileBinary, Attributes attrs)
155 			throws IOException {
156 		JarInputStream jarIn = null;
157 		try {
158 			jarIn = new JarInputStream(fileBinary.getStream());
159 			String bundleLocalization = null;
160 
161 			String blKey = Constants.BUNDLE_LOCALIZATION; // "Bundle-Localization";
162 			Name blkName = new Name(blKey);
163 
164 			browse: for (Object obj : attrs.keySet()) {
165 				String value = attrs.getValue((Attributes.Name) obj);
166 				if (value.startsWith("%")) {
167 					if (attrs.containsKey(blkName)) {
168 						bundleLocalization = attrs.getValue(blkName);
169 						break browse;
170 					}
171 				}
172 			}
173 
174 			JarEntry jarEntry = null;
175 			byte[] propBytes = null;
176 			ByteArrayOutputStream baos = null;
177 			browse: if (bundleLocalization != null) {
178 				JarEntry entry = jarIn.getNextJarEntry();
179 				while (entry != null) {
180 					if (entry.getName().equals(
181 							bundleLocalization + ".properties")) {
182 						jarEntry = entry;
183 
184 						// if(je.getSize() != -1){
185 						// propBytes = new byte[(int)je.getSize()];
186 						// int len = (int) je.getSize();
187 						// int offset = 0;
188 						// while (offset != len)
189 						// offset += jarIn.read(propBytes, offset, len -
190 						// offset);
191 						// } else {
192 						baos = new ByteArrayOutputStream();
193 						while (true) {
194 							int qwe = jarIn.read();
195 							if (qwe == -1)
196 								break;
197 							baos.write(qwe);
198 						}
199 						propBytes = baos.toByteArray();
200 						break browse;
201 					}
202 					entry = jarIn.getNextJarEntry();
203 				}
204 			}
205 
206 			if (jarEntry != null) {
207 				Properties prop = new Properties();
208 				InputStream is = new ByteArrayInputStream(propBytes);
209 				prop.load(is);
210 
211 				for (Object obj : attrs.keySet()) {
212 					String value = attrs.getValue((Attributes.Name) obj);
213 					if (value.startsWith("%")) {
214 						String newVal = prop.getProperty(value.substring(1));
215 						if (newVal != null)
216 							attrs.put(obj, newVal);
217 					}
218 				}
219 			}
220 		} catch (RepositoryException e) {
221 			throw new SlcException(
222 					"Error while reading the jar binary content " + fileBinary,
223 					e);
224 		} catch (IOException ioe) {
225 			throw new SlcException("unable to get internationalized values",
226 					ioe);
227 		} finally {
228 			IOUtils.closeQuietly(jarIn);
229 		}
230 	}
231 
232 	protected void addOsgiMetadata(Node fileNode, Attributes attrs)
233 			throws RepositoryException {
234 
235 		// TODO remove this ?
236 		// Compulsory for the time being, because bundle artifact extends
237 		// artifact
238 		if (!fileNode.isNodeType(SlcTypes.SLC_ARTIFACT)) {
239 			ArtifactIndexer indexer = new ArtifactIndexer();
240 			indexer.index(fileNode);
241 		}
242 
243 		fileNode.addMixin(SlcTypes.SLC_BUNDLE_ARTIFACT);
244 
245 		// symbolic name
246 		String symbolicName = attrs.getValue(Constants.BUNDLE_SYMBOLICNAME);
247 		// make sure there is no directive
248 		symbolicName = symbolicName.split(";")[0];
249 		fileNode.setProperty(SlcNames.SLC_SYMBOLIC_NAME, symbolicName);
250 
251 		// direct mapping
252 		addAttr(Constants.BUNDLE_SYMBOLICNAME, fileNode, attrs);
253 		addAttr(Constants.BUNDLE_NAME, fileNode, attrs);
254 		addAttr(Constants.BUNDLE_DESCRIPTION, fileNode, attrs);
255 		addAttr(Constants.BUNDLE_MANIFESTVERSION, fileNode, attrs);
256 		addAttr(Constants.BUNDLE_CATEGORY, fileNode, attrs);
257 		addAttr(Constants.BUNDLE_ACTIVATIONPOLICY, fileNode, attrs);
258 		addAttr(Constants.BUNDLE_COPYRIGHT, fileNode, attrs);
259 		addAttr(Constants.BUNDLE_VENDOR, fileNode, attrs);
260 		addAttr("Bundle-License", fileNode, attrs);
261 		addAttr(Constants.BUNDLE_DOCURL, fileNode, attrs);
262 		addAttr(Constants.BUNDLE_CONTACTADDRESS, fileNode, attrs);
263 		addAttr(Constants.BUNDLE_ACTIVATOR, fileNode, attrs);
264 		addAttr(Constants.BUNDLE_UPDATELOCATION, fileNode, attrs);
265 		addAttr(Constants.BUNDLE_LOCALIZATION, fileNode, attrs);
266 
267 		// required execution environment
268 		if (attrs.containsKey(new Name(
269 				Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)))
270 			fileNode.setProperty(SlcNames.SLC_
271 					+ Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT, attrs
272 					.getValue(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)
273 					.split(","));
274 
275 		// bundle classpath
276 		if (attrs.containsKey(new Name(Constants.BUNDLE_CLASSPATH)))
277 			fileNode.setProperty(SlcNames.SLC_ + Constants.BUNDLE_CLASSPATH,
278 					attrs.getValue(Constants.BUNDLE_CLASSPATH).split(","));
279 
280 		// version
281 		Version version = new Version(attrs.getValue(Constants.BUNDLE_VERSION));
282 		fileNode.setProperty(SlcNames.SLC_BUNDLE_VERSION, version.toString());
283 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.BUNDLE_VERSION);
284 		Node bundleVersionNode = fileNode.addNode(SlcNames.SLC_
285 				+ Constants.BUNDLE_VERSION, SlcTypes.SLC_OSGI_VERSION);
286 		mapOsgiVersion(version, bundleVersionNode);
287 
288 		// fragment
289 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.FRAGMENT_HOST);
290 		if (attrs.containsKey(new Name(Constants.FRAGMENT_HOST))) {
291 			String fragmentHost = attrs.getValue(Constants.FRAGMENT_HOST);
292 			String[] tokens = fragmentHost.split(";");
293 			Node node = fileNode.addNode(SlcNames.SLC_
294 					+ Constants.FRAGMENT_HOST, SlcTypes.SLC_FRAGMENT_HOST);
295 			node.setProperty(SlcNames.SLC_SYMBOLIC_NAME, tokens[0]);
296 			for (int i = 1; i < tokens.length; i++) {
297 				if (tokens[i].startsWith(Constants.BUNDLE_VERSION_ATTRIBUTE)) {
298 					node.setProperty(SlcNames.SLC_BUNDLE_VERSION,
299 							attributeValue(tokens[i]));
300 				}
301 			}
302 		}
303 
304 		// imported packages
305 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.IMPORT_PACKAGE);
306 		if (attrs.containsKey(new Name(Constants.IMPORT_PACKAGE))) {
307 			String importPackages = attrs.getValue(Constants.IMPORT_PACKAGE);
308 			List<String> packages = parseCommaSeparated(importPackages);
309 			for (String pkg : packages) {
310 				String[] tokens = pkg.split(";");
311 				Node node = fileNode.addNode(SlcNames.SLC_
312 						+ Constants.IMPORT_PACKAGE,
313 						SlcTypes.SLC_IMPORTED_PACKAGE);
314 				node.setProperty(SlcNames.SLC_NAME, tokens[0]);
315 				for (int i = 1; i < tokens.length; i++) {
316 					if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
317 						node.setProperty(SlcNames.SLC_VERSION,
318 								attributeValue(tokens[i]));
319 					} else if (tokens[i]
320 							.startsWith(Constants.RESOLUTION_DIRECTIVE)) {
321 						node.setProperty(
322 								SlcNames.SLC_OPTIONAL,
323 								directiveValue(tokens[i]).equals(
324 										Constants.RESOLUTION_OPTIONAL));
325 					}
326 				}
327 			}
328 		}
329 
330 		// dynamic import package
331 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.DYNAMICIMPORT_PACKAGE);
332 		if (attrs.containsKey(new Name(Constants.DYNAMICIMPORT_PACKAGE))) {
333 			String importPackages = attrs
334 					.getValue(Constants.DYNAMICIMPORT_PACKAGE);
335 			List<String> packages = parseCommaSeparated(importPackages);
336 			for (String pkg : packages) {
337 				String[] tokens = pkg.split(";");
338 				Node node = fileNode.addNode(SlcNames.SLC_
339 						+ Constants.DYNAMICIMPORT_PACKAGE,
340 						SlcTypes.SLC_DYNAMIC_IMPORTED_PACKAGE);
341 				node.setProperty(SlcNames.SLC_NAME, tokens[0]);
342 				for (int i = 1; i < tokens.length; i++) {
343 					if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
344 						node.setProperty(SlcNames.SLC_VERSION,
345 								attributeValue(tokens[i]));
346 					}
347 				}
348 			}
349 		}
350 
351 		// exported packages
352 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.EXPORT_PACKAGE);
353 		if (attrs.containsKey(new Name(Constants.EXPORT_PACKAGE))) {
354 			String exportPackages = attrs.getValue(Constants.EXPORT_PACKAGE);
355 			List<String> packages = parseCommaSeparated(exportPackages);
356 			for (String pkg : packages) {
357 				String[] tokens = pkg.split(";");
358 				Node node = fileNode.addNode(SlcNames.SLC_
359 						+ Constants.EXPORT_PACKAGE,
360 						SlcTypes.SLC_EXPORTED_PACKAGE);
361 				node.setProperty(SlcNames.SLC_NAME, tokens[0]);
362 				// TODO: are these cleans really necessary?
363 				cleanSubNodes(node, SlcNames.SLC_USES);
364 				cleanSubNodes(node, SlcNames.SLC_VERSION);
365 				for (int i = 1; i < tokens.length; i++) {
366 					if (tokens[i].startsWith(Constants.VERSION_ATTRIBUTE)) {
367 						String versionStr = attributeValue(tokens[i]);
368 						Node versionNode = node.addNode(SlcNames.SLC_VERSION,
369 								SlcTypes.SLC_OSGI_VERSION);
370 						mapOsgiVersion(new Version(versionStr), versionNode);
371 					} else if (tokens[i].startsWith(Constants.USES_DIRECTIVE)) {
372 						String usedPackages = directiveValue(tokens[i]);
373 						// log.debug("uses='" + usedPackages + "'");
374 						for (String usedPackage : usedPackages.split(",")) {
375 							// log.debug("usedPackage='" +
376 							// usedPackage +
377 							// "'");
378 							Node usesNode = node.addNode(SlcNames.SLC_USES,
379 									SlcTypes.SLC_JAVA_PACKAGE);
380 							usesNode.setProperty(SlcNames.SLC_NAME, usedPackage);
381 						}
382 					}
383 				}
384 			}
385 		}
386 
387 		// required bundle
388 		cleanSubNodes(fileNode, SlcNames.SLC_ + Constants.REQUIRE_BUNDLE);
389 		if (attrs.containsKey(new Name(Constants.REQUIRE_BUNDLE))) {
390 			String requireBundle = attrs.getValue(Constants.REQUIRE_BUNDLE);
391 			List<String> bundles = parseCommaSeparated(requireBundle);
392 			for (String bundle : bundles) {
393 				String[] tokens = bundle.split(";");
394 				Node node = fileNode.addNode(SlcNames.SLC_
395 						+ Constants.REQUIRE_BUNDLE,
396 						SlcTypes.SLC_REQUIRED_BUNDLE);
397 				node.setProperty(SlcNames.SLC_SYMBOLIC_NAME, tokens[0]);
398 				for (int i = 1; i < tokens.length; i++) {
399 					if (tokens[i]
400 							.startsWith(Constants.BUNDLE_VERSION_ATTRIBUTE)) {
401 						node.setProperty(SlcNames.SLC_BUNDLE_VERSION,
402 								attributeValue(tokens[i]));
403 					} else if (tokens[i]
404 							.startsWith(Constants.RESOLUTION_DIRECTIVE)) {
405 						node.setProperty(
406 								SlcNames.SLC_OPTIONAL,
407 								directiveValue(tokens[i]).equals(
408 										Constants.RESOLUTION_OPTIONAL));
409 					}
410 				}
411 			}
412 		}
413 
414 	}
415 
416 	private void addAttr(String key, Node node, Attributes attrs)
417 			throws RepositoryException {
418 		addAttr(new Name(key), node, attrs);
419 	}
420 
421 	private void addAttr(Name key, Node node, Attributes attrs)
422 			throws RepositoryException {
423 		if (attrs.containsKey(key)) {
424 			String value = attrs.getValue(key);
425 			node.setProperty(SlcNames.SLC_ + key, value);
426 		}
427 	}
428 
429 	private void cleanSubNodes(Node node, String name)
430 			throws RepositoryException {
431 		if (node.hasNode(name)) {
432 			NodeIterator nit = node.getNodes(name);
433 			while (nit.hasNext())
434 				nit.nextNode().remove();
435 		}
436 	}
437 
438 	private String attributeValue(String str) {
439 		return extractValue(str, "=");
440 	}
441 
442 	private String directiveValue(String str) {
443 		return extractValue(str, ":=");
444 	}
445 
446 	private String extractValue(String str, String eq) {
447 		String[] tokens = str.split(eq);
448 		// String key = tokens[0];
449 		String value = tokens[1].trim();
450 		// TODO: optimize?
451 		if (value.startsWith("\""))
452 			value = value.substring(1);
453 		if (value.endsWith("\""))
454 			value = value.substring(0, value.length() - 1);
455 		return value;
456 	}
457 
458 	/** Parse package list with nested directive with ',' */
459 	private List<String> parseCommaSeparated(String str) {
460 		List<String> res = new ArrayList<String>();
461 		StringBuffer curr = new StringBuffer("");
462 		boolean in = false;
463 		for (char c : str.toCharArray()) {
464 			if (c == ',') {
465 				if (!in) {// new package
466 					res.add(curr.toString());
467 					curr = new StringBuffer("");
468 				} else {// a ',' within " "
469 					curr.append(c);
470 				}
471 			} else if (c == '\"') {
472 				in = !in;
473 				curr.append(c);
474 			} else {
475 				curr.append(c);
476 			}
477 		}
478 		res.add(curr.toString());
479 		// log.debug(res);
480 		return res;
481 	}
482 
483 	protected void mapOsgiVersion(Version version, Node versionNode)
484 			throws RepositoryException {
485 		versionNode.setProperty(SlcNames.SLC_AS_STRING, version.toString());
486 		versionNode.setProperty(SlcNames.SLC_MAJOR, version.getMajor());
487 		versionNode.setProperty(SlcNames.SLC_MINOR, version.getMinor());
488 		versionNode.setProperty(SlcNames.SLC_MICRO, version.getMicro());
489 		if (!version.getQualifier().equals(""))
490 			versionNode.setProperty(SlcNames.SLC_QUALIFIER,
491 					version.getQualifier());
492 	}
493 
494 	public void setForce(Boolean force) {
495 		this.force = force;
496 	}
497 
498 }