/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.felix.ipojo.manipulator.store;
import org.apache.felix.ipojo.manipulator.ResourceStore;
import org.apache.felix.ipojo.manipulator.ResourceVisitor;
import org.apache.felix.ipojo.manipulator.store.mapper.IdentityResourceMapper;
import org.apache.felix.ipojo.manipulator.util.Metadatas;
import org.apache.felix.ipojo.manipulator.util.Streams;
import org.apache.felix.ipojo.metadata.Element;
import java.io.*;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* A {@link JarFileResourceStore} knows how to read and write
* resources from (to respectively) a Jar File.
*
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*/
public class JarFileResourceStore implements ResourceStore {
/**
* Source Jar.
*/
private JarFile m_source;
/**
* Target File.
*/
private File m_target;
/**
* Modified resources.
*/
private Map<String, byte[]> m_content;
/**
* Resource Mapper.
*/
private ResourceMapper m_mapper = new IdentityResourceMapper();
/**
* The builder of the updated manifest.
*/
private ManifestBuilder m_manifestBuilder;
/**
* Original manifest to be updated.
*/
private Manifest m_manifest;
private ClassLoader classLoader;
/**
* Construct a {@link JarFileResourceStore} wrapping the given original bundle,
* and configured to output in the given target file.
*
* @param source original Bundle
* @param target File where the updated Bundle will be outputted
* @throws IOException if there is an error retrieving the Manifest from the original JarFile
*/
public JarFileResourceStore(JarFile source, File target) throws IOException {
m_source = source;
m_target = target;
// TODO ensure File is not null and not an existing file/directory
this.m_target = target;
if (source != null) {
m_manifest = source.getManifest();
} else {
m_manifest = new Manifest();
}
m_content = new TreeMap<String, byte[]>();
}
public void setResourceMapper(ResourceMapper mapper) {
this.m_mapper = mapper;
}
public void setManifestBuilder(ManifestBuilder manifestBuilder) {
this.m_manifestBuilder = manifestBuilder;
}
public void setManifest(Manifest manifest) {
this.m_manifest = manifest;
}
public void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
public byte[] read(String path) throws IOException {
ZipEntry entry = m_source.getEntry(getInternalPath(path));
if (entry == null) {
// Not in the Jar file, trying from classpath
return tryToLoadFromClassloader(path);
}
return Streams.readBytes(m_source.getInputStream(entry));
}
private byte[] tryToLoadFromClassloader(String path) throws IOException {
if (classLoader != null) {
byte[] bytes = toByteArray(classLoader.getResource(path));
if (bytes != null) {
return bytes;
}
}
throw new IOException("Class not found " + path + ".");
}
public static byte[] toByteArray(URL url) throws IOException {
if (url == null) {
return null;
}
InputStream input = url.openStream();
try {
return Streams.readBytes(input);
} finally {
Streams.close(input);
}
}
private String getInternalPath(String path) {
return m_mapper.internalize(path);
}
public void accept(ResourceVisitor visitor) {
List<JarEntry> entries = Collections.list(m_source.entries());
for (JarEntry entry : entries) {
String name = entry.getName();
if (!name.endsWith("/")) {
// Do not visit directories
visitor.visit(getExternalName(entry.getName()));
}
}
}
private String getExternalName(String path) {
return m_mapper.externalize(path);
}
public void open() throws IOException {
// Nothing to do
}
public void writeMetadata(Element metadata) {
m_manifestBuilder.addMetada(Collections.singletonList(metadata));
m_manifestBuilder.addReferredPackage(Metadatas.findReferredPackages(metadata));
}
public void write(String resourcePath, byte[] resource) throws IOException {
this.m_content.put(getInternalPath(resourcePath), resource);
}
public void close() throws IOException {
// Update the manifest
Manifest updated = m_manifestBuilder.build(m_manifest);
// Create a new Jar file
FileOutputStream fos = new FileOutputStream(m_target);
JarOutputStream jos = new JarOutputStream(fos, updated);
try {
// Copy classes and resources
List<JarEntry> entries = Collections.list(m_source.entries());
for (JarEntry entry : entries) {
// Ignore some entries (MANIFEST, ...)
if (isIgnored(entry)) {
continue;
}
if (isUpdated(entry)) {
// Write newer/updated resource (manipulated classes, ...)
JarEntry je = new JarEntry(entry.getName());
byte[] data = m_content.get(getInternalPath(entry.getName()));
jos.putNextEntry(je);
jos.write(data);
jos.closeEntry();
} else {
// Copy the resource as-is
jos.putNextEntry(entry);
InputStream is = m_source.getInputStream(entry);
try {
Streams.transfer(is, jos);
} finally {
Streams.close(is);
}
jos.closeEntry();
}
}
} finally {
try {
m_source.close();
} catch (IOException e) {
// Ignored
}
Streams.close(jos, fos);
}
}
private boolean isUpdated(JarEntry entry) {
// Check if that class was manipulated
if (entry.getName().endsWith(".class")) {
// Need to map this into an external+normalized path
String cleaned = getExternalName(entry.getName());
return m_content.containsKey(cleaned);
} else {
return false;
}
}
private boolean isIgnored(JarEntry entry) {
return "META-INF/MANIFEST.MF".equals(entry.getName());
}
}