1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.catalina.webresources;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.MalformedURLException;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.jar.JarEntry;
28 import java.util.jar.JarFile;
29 import java.util.jar.JarInputStream;
30 import java.util.jar.Manifest;
31
32 import org.apache.catalina.LifecycleException;
33 import org.apache.catalina.WebResource;
34 import org.apache.catalina.WebResourceRoot;
35 import org.apache.tomcat.util.buf.UriUtil;
36 import org.apache.tomcat.util.compat.JreCompat;
37
38 /**
39  * Represents a {@link org.apache.catalina.WebResourceSet} based on a JAR file
40  * that is nested inside a packed WAR file. This is only intended for internal
41  * use within Tomcat and therefore cannot be created via configuration.
42  */

43 public class JarWarResourceSet extends AbstractArchiveResourceSet {
44
45     private final String archivePath;
46
47     /**
48      * Creates a new {@link org.apache.catalina.WebResourceSet} based on a JAR
49      * file that is nested inside a WAR.
50      *
51      * @param root          The {@link WebResourceRoot} this new
52      *                          {@link org.apache.catalina.WebResourceSet} will
53      *                          be added to.
54      * @param webAppMount   The path within the web application at which this
55      *                          {@link org.apache.catalina.WebResourceSet} will
56      *                          be mounted.
57      * @param base          The absolute path to the WAR file on the file system
58      *                          in which the JAR is located.
59      * @param archivePath   The path within the WAR file where the JAR file is
60      *                          located.
61      * @param internalPath  The path within this new {@link
62      *                          org.apache.catalina.WebResourceSet} where
63      *                          resources will be served from. E.g. for a
64      *                          resource JAR, this would be "META-INF/resources"
65      *
66      * @throws IllegalArgumentException if the webAppMount or internalPath is
67      *         not valid (valid paths must start with '/')
68      */

69     public JarWarResourceSet(WebResourceRoot root, String webAppMount,
70             String base, String archivePath, String internalPath)
71             throws IllegalArgumentException {
72         setRoot(root);
73         setWebAppMount(webAppMount);
74         setBase(base);
75         this.archivePath = archivePath;
76         setInternalPath(internalPath);
77
78         if (getRoot().getState().isAvailable()) {
79             try {
80                 start();
81             } catch (LifecycleException e) {
82                 throw new IllegalStateException(e);
83             }
84         }
85     }
86
87     @Override
88     protected WebResource createArchiveResource(JarEntry jarEntry,
89             String webAppPath, Manifest manifest) {
90         return new JarWarResource(this, webAppPath, getBaseUrlString(), jarEntry, archivePath);
91     }
92
93
94     /**
95      * {@inheritDoc}
96      * <p>
97      * JarWar can't optimise for a single resource so the Map is always
98      * returned.
99      */

100     @Override
101     protected Map<String,JarEntry> getArchiveEntries(boolean single) {
102         synchronized (archiveLock) {
103             if (archiveEntries == null) {
104                 JarFile warFile = null;
105                 InputStream jarFileIs = null;
106                 archiveEntries = new HashMap<>();
107                 boolean multiRelease = false;
108                 try {
109                     warFile = openJarFile();
110                     JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
111                     jarFileIs = warFile.getInputStream(jarFileInWar);
112
113                     try (TomcatJarInputStream jarIs = new TomcatJarInputStream(jarFileIs)) {
114                         JarEntry entry = jarIs.getNextJarEntry();
115                         while (entry != null) {
116                             archiveEntries.put(entry.getName(), entry);
117                             entry = jarIs.getNextJarEntry();
118                         }
119                         Manifest m = jarIs.getManifest();
120                         setManifest(m);
121                         if (m != null && JreCompat.isJre9Available()) {
122                             String value = m.getMainAttributes().getValue("Multi-Release");
123                             if (value != null) {
124                                 multiRelease = Boolean.parseBoolean(value);
125                             }
126                         }
127                         // Hack to work-around JarInputStream swallowing these
128                         // entries. TomcatJarInputStream is used above which
129                         // extends JarInputStream and the method that creates
130                         // the entries over-ridden so we can a) tell if the
131                         // entries are present and b) cache them so we can
132                         // access them here.
133                         entry = jarIs.getMetaInfEntry();
134                         if (entry != null) {
135                             archiveEntries.put(entry.getName(), entry);
136                         }
137                         entry = jarIs.getManifestEntry();
138                         if (entry != null) {
139                             archiveEntries.put(entry.getName(), entry);
140                         }
141                     }
142                     if (multiRelease) {
143                         processArchivesEntriesForMultiRelease();
144                     }
145                 } catch (IOException ioe) {
146                     // Should never happen
147                     archiveEntries = null;
148                     throw new IllegalStateException(ioe);
149                 } finally {
150                     if (warFile != null) {
151                         closeJarFile();
152                     }
153                     if (jarFileIs != null) {
154                         try {
155                             jarFileIs.close();
156                         } catch (IOException e) {
157                             // Ignore
158                         }
159                     }
160                 }
161             }
162             return archiveEntries;
163         }
164     }
165
166
167     protected void processArchivesEntriesForMultiRelease() {
168
169         int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion();
170
171         Map<String,VersionedJarEntry> versionedEntries = new HashMap<>();
172         Iterator<Entry<String,JarEntry>> iter = archiveEntries.entrySet().iterator();
173         while (iter.hasNext()) {
174             Entry<String,JarEntry> entry = iter.next();
175             String name = entry.getKey();
176             if (name.startsWith("META-INF/versions/")) {
177                 // Remove the multi-release version
178                 iter.remove();
179
180                 // Get the base name and version for this versioned entry
181                 int i = name.indexOf('/', 18);
182                 if (i > 0) {
183                     String baseName = name.substring(i + 1);
184                     int version = Integer.parseInt(name.substring(18, i));
185
186                     // Ignore any entries targeting for a later version than
187                     // the target for this runtime
188                     if (version <= targetVersion) {
189                         VersionedJarEntry versionedJarEntry = versionedEntries.get(baseName);
190                         if (versionedJarEntry == null) {
191                             // No versioned entry found for this name. Create
192                             // one.
193                             versionedEntries.put(baseName,
194                                     new VersionedJarEntry(version, entry.getValue()));
195                         } else {
196                             // Ignore any entry for which we have already found
197                             // a later version
198                             if (version > versionedJarEntry.getVersion()) {
199                                 // Replace the entry targeted at an earlier
200                                 // version
201                                 versionedEntries.put(baseName,
202                                         new VersionedJarEntry(version, entry.getValue()));
203                             }
204                         }
205                     }
206                 }
207             }
208         }
209
210         for (Entry<String,VersionedJarEntry> versionedJarEntry : versionedEntries.entrySet()) {
211             archiveEntries.put(versionedJarEntry.getKey(),
212                     versionedJarEntry.getValue().getJarEntry());
213         }
214     }
215
216
217     /**
218      * {@inheritDoc}
219      * <p>
220      * Should never be called since {@link #getArchiveEntries(boolean)} always
221      * returns a Map.
222      */

223     @Override
224     protected JarEntry getArchiveEntry(String pathInArchive) {
225         throw new IllegalStateException(sm.getString("jarWarResourceSet.codingError"));
226     }
227
228
229     @Override
230     protected boolean isMultiRelease() {
231         // This always returns false otherwise the superclass will call
232         // #getArchiveEntry(String)
233         return false;
234     }
235
236
237     //-------------------------------------------------------- Lifecycle methods
238     @Override
239     protected void initInternal() throws LifecycleException {
240
241         try (JarFile warFile = new JarFile(getBase())) {
242             JarEntry jarFileInWar = warFile.getJarEntry(archivePath);
243             InputStream jarFileIs = warFile.getInputStream(jarFileInWar);
244
245             try (JarInputStream jarIs = new JarInputStream(jarFileIs)) {
246                 setManifest(jarIs.getManifest());
247             }
248         } catch (IOException ioe) {
249             throw new IllegalArgumentException(ioe);
250         }
251
252         try {
253             setBaseUrl(UriUtil.buildJarSafeUrl(new File(getBase())));
254         } catch (MalformedURLException e) {
255             throw new IllegalArgumentException(e);
256         }
257     }
258
259
260     private static final class VersionedJarEntry {
261         private final int version;
262         private final JarEntry jarEntry;
263
264         public VersionedJarEntry(int version, JarEntry jarEntry) {
265             this.version = version;
266             this.jarEntry = jarEntry;
267         }
268
269
270         public int getVersion() {
271             return version;
272         }
273
274
275         public JarEntry getJarEntry() {
276             return jarEntry;
277         }
278     }
279 }
280