/*
* Copyright 2008 Alin Dreghiciu.
* Copyright 2008 Peter Kriens.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ops4j.pax.swissbox.bnd;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Properties;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.ops4j.lang.NullArgumentException;
import aQute.lib.osgi.Analyzer;
import aQute.lib.osgi.Jar;
/**
* Wrapper over PeterK's bnd lib.
*
* @author Alin Dreghiciu
* @since 0.1.0, January 14, 2008
*/
public class BndUtils {
/**
* Regex pattern for matching instructions when specified in url.
*/
private static final Pattern INSTRUCTIONS_PATTERN = Pattern
.compile("([a-zA-Z_0-9-]+)=([-!\"'()*+,.0-9A-Z_a-z%;:=/]+)");
/**
* Utility class. Ment to be used using static methods
*/
private BndUtils() {
// utility class
}
/**
* Processes the input jar and generates the necessary OSGi headers using
* specified instructions.
*
* @param jarInputStream
* input stream for the jar to be processed. Cannot be null.
* @param instructions
* bnd specific processing instructions. Cannot be null.
* @param jarInfo
* information about the jar to be processed. Usually the jar
* url. Cannot be null or empty.
*
* @return an input stream for the generated bundle
*
* @throws NullArgumentException
* if any of the parameters is null
* @throws IOException
* re-thron during jar processing
*/
public static InputStream createBundle(final InputStream jarInputStream,
final Properties instructions, final String jarInfo)
throws Exception {
return createBundle(jarInputStream, instructions, jarInfo,
OverwriteMode.KEEP);
}
/**
* Processes the input jar and generates the necessary OSGi headers using
* specified instructions.
*
* @param jarInputStream
* input stream for the jar to be processed. Cannot be null.
* @param instructions
* bnd specific processing instructions. Cannot be null.
* @param jarInfo
* information about the jar to be processed. Usually the jar
* url. Cannot be null or empty.
* @param overwriteMode
* manifets overwrite mode
*
* @return an input stream for the generated bundle
*
* @throws NullArgumentException
* if any of the parameters is null
* @throws IOException
* re-thron during jar processing
*/
public static InputStream createBundle(final InputStream jarInputStream,
final Properties instructions, final String jarInfo,
final OverwriteMode overwriteMode) throws Exception {
NullArgumentException.validateNotNull(jarInputStream, "Jar URL");
NullArgumentException.validateNotNull(instructions, "Instructions");
NullArgumentException.validateNotEmpty(jarInfo, "Jar info");
// LOG.debug( "Creating bundle for [" + jarInfo + "]" );
// LOG.debug( "Overwrite mode: " + overwriteMode );
// LOG.trace( "Using instructions " + instructions );
// Do support jar as directory.
Jar jar = null;
URL jarURL = new URL(jarInfo);
if (jarURL.getProtocol().equals("file")) {
File jarFileOrDir = new File(jarURL.getPath());
if (jarFileOrDir.isDirectory())
jar = new Jar(jarURL.getPath(), jarFileOrDir);
}
if (jar == null)
jar = new Jar("dot", jarInputStream);
final Manifest manifest = jar.getManifest();
// Make the jar a bundle if it is not already a bundle
if (manifest == null
|| OverwriteMode.KEEP != overwriteMode
|| (manifest.getMainAttributes().getValue(
Analyzer.EXPORT_PACKAGE) == null && manifest
.getMainAttributes().getValue(Analyzer.IMPORT_PACKAGE) == null)) {
// Do not use instructions as default for properties because it
// looks like BND uses the props
// via some other means then getProperty() and so the instructions
// will not be used at all
// So, just copy instructions to properties
final Properties properties = new Properties();
properties.putAll(instructions);
properties.put("Generated-By-Ops4j-Pax-From", jarInfo);
final Analyzer analyzer = new Analyzer();
analyzer.setJar(jar);
analyzer.setProperties(properties);
if (manifest != null && OverwriteMode.MERGE == overwriteMode) {
analyzer.mergeManifest(manifest);
}
checkMandatoryProperties(analyzer, jar, jarInfo);
analyzer.calcManifest();
}
return createInputStream(jar);
}
/**
* Creates an piped input stream for the wrapped jar. This is done in a
* thread so we can return quickly.
*
* @param jar
* the wrapped jar
*
* @return an input stream for the wrapped jar
*
* @throws java.io.IOException
* re-thrown
*/
private static PipedInputStream createInputStream(final Jar jar)
throws IOException {
final PipedInputStream pin = new PipedInputStream();
final PipedOutputStream pout = new PipedOutputStream(pin);
new Thread() {
@Override
public void run() {
try {
jar.write(pout);
} catch (Exception e) {
// LOG.warn( "Bundle cannot be generated" );
} finally {
try {
jar.close();
pout.close();
} catch (IOException ignore) {
// if we get here something is very wrong
// LOG.error("Bundle cannot be generated", ignore);
}
}
}
}.start();
return pin;
}
/**
* Check if manadatory properties are present, otherwise generate default.
*
* @param analyzer
* bnd analyzer
* @param jar
* bnd jar
* @param symbolicName
* bundle symbolic name
*/
private static void checkMandatoryProperties(final Analyzer analyzer,
final Jar jar, final String symbolicName) {
final String importPackage = analyzer
.getProperty(Analyzer.IMPORT_PACKAGE);
if (importPackage == null || importPackage.trim().length() == 0) {
analyzer.setProperty(Analyzer.IMPORT_PACKAGE,
"*;resolution:=optional");
}
final String exportPackage = analyzer
.getProperty(Analyzer.EXPORT_PACKAGE);
if (exportPackage == null || exportPackage.trim().length() == 0) {
analyzer.setProperty(Analyzer.EXPORT_PACKAGE, analyzer
.calculateExportsFromContents(jar));
}
final String localSymbolicName = analyzer.getProperty(
Analyzer.BUNDLE_SYMBOLICNAME, symbolicName);
analyzer.setProperty(Analyzer.BUNDLE_SYMBOLICNAME,
generateSymbolicName(localSymbolicName));
}
/**
* Processes symbolic name and replaces osgi spec invalid characters with
* "_".
*
* @param symbolicName
* bundle symbolic name
*
* @return a valid symbolic name
*/
private static String generateSymbolicName(final String symbolicName) {
return symbolicName.replaceAll("[^a-zA-Z_0-9.-]", "_");
}
/**
* Parses bnd instructions out of an url query string.
*
* @param query
* query part of an url.
*
* @return parsed instructions as properties
*
* @throws java.net.MalformedURLException
* if provided path does not comply to syntax.
*/
public static Properties parseInstructions(final String query)
throws MalformedURLException {
final Properties instructions = new Properties();
if (query != null) {
try {
// just ignore for the moment and try out if we have valid
// properties separated by "&"
final String segments[] = query.split("&");
for (String segment : segments) {
// do not parse empty strings
if (segment.trim().length() > 0) {
final Matcher matcher = INSTRUCTIONS_PATTERN
.matcher(segment);
if (matcher.matches()) {
instructions.setProperty(matcher.group(1),
URLDecoder
.decode(matcher.group(2), "UTF-8"));
} else {
throw new MalformedURLException(
"Invalid syntax for instruction ["
+ segment
+ "]. Take a look at http://www.aqute.biz/Code/Bnd.");
}
}
}
} catch (UnsupportedEncodingException e) {
// thrown by URLDecoder but it should never happen
throwAsMalformedURLException(
"Could not retrieve the instructions from [" + query
+ "]", e);
}
}
return instructions;
}
/**
* Creates an MalformedURLException with a message and a cause.
*
* @param message
* exception message
* @param cause
* exception cause
*
* @throws MalformedURLException
* the created MalformedURLException
*/
private static void throwAsMalformedURLException(final String message,
final Exception cause) throws MalformedURLException {
final MalformedURLException exception = new MalformedURLException(
message);
exception.initCause(cause);
throw exception;
}
}