| /* |
| * 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.tomcat.util.scan; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.jar.JarEntry; |
| import java.util.jar.Manifest; |
| |
| import org.apache.tomcat.Jar; |
| import org.apache.tomcat.util.compat.JreCompat; |
| |
| /** |
| * Base implementation of Jar for implementations that use a JarInputStream to |
| * access the JAR file. |
| */ |
| public abstract class AbstractInputStreamJar implements Jar { |
| |
| private final URL jarFileURL; |
| |
| private NonClosingJarInputStream jarInputStream = null; |
| private JarEntry entry = null; |
| private Boolean multiRelease = null; |
| private Map<String,String> mrMap = null; |
| |
| public AbstractInputStreamJar(URL jarFileUrl) { |
| this.jarFileURL = jarFileUrl; |
| } |
| |
| |
| @Override |
| public URL getJarFileURL() { |
| return jarFileURL; |
| } |
| |
| |
| @Override |
| public void nextEntry() { |
| if (jarInputStream == null) { |
| try { |
| reset(); |
| } catch (IOException e) { |
| entry = null; |
| return; |
| } |
| } |
| try { |
| entry = jarInputStream.getNextJarEntry(); |
| if (multiRelease.booleanValue()) { |
| // Skip base entries where there is a multi-release entry |
| // Skip multi-release entries that are not being used |
| while (entry != null && |
| (mrMap.keySet().contains(entry.getName()) || |
| entry.getName().startsWith("META-INF/versions/") && |
| !mrMap.values().contains(entry.getName()))) { |
| entry = jarInputStream.getNextJarEntry(); |
| } |
| } else { |
| // Skip multi-release entries |
| while (entry != null && entry.getName().startsWith("META-INF/versions/")) { |
| entry = jarInputStream.getNextJarEntry(); |
| } |
| } |
| } catch (IOException ioe) { |
| entry = null; |
| } |
| } |
| |
| |
| @Override |
| public String getEntryName() { |
| // Given how the entry name is used, there is no requirement to convert |
| // the name for a multi-release entry to the corresponding base name. |
| if (entry == null) { |
| return null; |
| } else { |
| return entry.getName(); |
| } |
| } |
| |
| |
| @Override |
| public InputStream getEntryInputStream() throws IOException { |
| return jarInputStream; |
| } |
| |
| |
| @Override |
| @Deprecated |
| public boolean entryExists(String name) throws IOException { |
| return false; |
| } |
| |
| |
| @Override |
| public InputStream getInputStream(String name) throws IOException { |
| gotoEntry(name); |
| if (entry == null) { |
| return null; |
| } else { |
| // Clear the entry so that multiple calls to this method for the |
| // same entry will result in a new InputStream for each call |
| // (BZ 60798) |
| entry = null; |
| return jarInputStream; |
| } |
| } |
| |
| |
| @Override |
| public long getLastModified(String name) throws IOException { |
| gotoEntry(name); |
| if (entry == null) { |
| return -1; |
| } else { |
| return entry.getTime(); |
| } |
| } |
| |
| |
| @Override |
| public String getURL(String entry) { |
| StringBuilder result = new StringBuilder("jar:"); |
| result.append(getJarFileURL().toExternalForm()); |
| result.append("!/"); |
| result.append(entry); |
| |
| return result.toString(); |
| } |
| |
| |
| @Override |
| public Manifest getManifest() throws IOException { |
| reset(); |
| return jarInputStream.getManifest(); |
| } |
| |
| |
| @Override |
| public void reset() throws IOException { |
| closeStream(); |
| entry = null; |
| jarInputStream = createJarInputStream(); |
| // Only perform multi-release processing on first access |
| if (multiRelease == null) { |
| if (JreCompat.isJre9Available()) { |
| Manifest manifest = jarInputStream.getManifest(); |
| if (manifest == null) { |
| multiRelease = Boolean.FALSE; |
| } else { |
| String mrValue = manifest.getMainAttributes().getValue("Multi-Release"); |
| if (mrValue == null) { |
| multiRelease = Boolean.FALSE; |
| } else { |
| multiRelease = Boolean.valueOf(mrValue); |
| } |
| } |
| } else { |
| multiRelease = Boolean.FALSE; |
| } |
| if (multiRelease.booleanValue()) { |
| if (mrMap == null) { |
| populateMrMap(); |
| } |
| } |
| } |
| } |
| |
| |
| protected void closeStream() { |
| if (jarInputStream != null) { |
| try { |
| jarInputStream.reallyClose(); |
| } catch (IOException ioe) { |
| // Ignore |
| } |
| } |
| } |
| |
| |
| protected abstract NonClosingJarInputStream createJarInputStream() throws IOException; |
| |
| |
| private void gotoEntry(String name) throws IOException { |
| boolean needsReset = true; |
| if (multiRelease == null) { |
| reset(); |
| needsReset = false; |
| } |
| |
| // Need to convert requested name to multi-release name (if one exists) |
| if (multiRelease.booleanValue()) { |
| String mrName = mrMap.get(name); |
| if (mrName != null) { |
| name = mrName; |
| } |
| } else if (name.startsWith("META-INF/versions/")) { |
| entry = null; |
| return; |
| } |
| |
| if (entry != null && name.equals(entry.getName())) { |
| return; |
| } |
| if (needsReset) { |
| reset(); |
| } |
| |
| JarEntry jarEntry = jarInputStream.getNextJarEntry(); |
| while (jarEntry != null) { |
| if (name.equals(jarEntry.getName())) { |
| entry = jarEntry; |
| break; |
| } |
| jarEntry = jarInputStream.getNextJarEntry(); |
| } |
| } |
| |
| |
| private void populateMrMap() throws IOException { |
| int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion(); |
| |
| Map<String,Integer> mrVersions = new HashMap<>(); |
| |
| JarEntry jarEntry = jarInputStream.getNextJarEntry(); |
| |
| // Tracking the base name and the latest valid version found is |
| // sufficient to be able to create the renaming map required |
| while (jarEntry != null) { |
| String name = jarEntry.getName(); |
| if (name.startsWith("META-INF/versions/") && name.endsWith(".class")) { |
| |
| // Get the base name and version for this versioned entry |
| int i = name.indexOf('/', 18); |
| if (i > 0) { |
| String baseName = name.substring(i + 1); |
| int version = Integer.parseInt(name.substring(18, i)); |
| |
| // Ignore any entries targeting for a later version than |
| // the target for this runtime |
| if (version <= targetVersion) { |
| Integer mappedVersion = mrVersions.get(baseName); |
| if (mappedVersion == null) { |
| // No version found for this name. Create one. |
| mrVersions.put(baseName, Integer.valueOf(version)); |
| } else { |
| // Ignore any entry for which we have already found |
| // a later version |
| if (version > mappedVersion.intValue()) { |
| // Replace the earlier version |
| mrVersions.put(baseName, Integer.valueOf(version)); |
| } |
| } |
| } |
| } |
| } |
| jarEntry = jarInputStream.getNextJarEntry(); |
| } |
| |
| mrMap = new HashMap<>(); |
| |
| for (Entry<String,Integer> mrVersion : mrVersions.entrySet()) { |
| mrMap.put(mrVersion.getKey() , "META-INF/versions/" + mrVersion.getValue().toString() + |
| "/" + mrVersion.getKey()); |
| } |
| |
| // Reset stream back to the beginning of the JAR |
| closeStream(); |
| jarInputStream = createJarInputStream(); |
| } |
| } |