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.ByteArrayInputStream;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileNotFoundException;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Files;
29 import java.nio.file.attribute.BasicFileAttributes;
30 import java.security.cert.Certificate;
31 import java.util.jar.Manifest;
32
33 import org.apache.catalina.WebResourceRoot;
34 import org.apache.juli.logging.Log;
35 import org.apache.juli.logging.LogFactory;
36
37 /**
38  * Represents a single resource (file or directory) that is located on a file
39  * system.
40  */

41 public class FileResource extends AbstractResource {
42
43     private static final Log log = LogFactory.getLog(FileResource.class);
44
45     private static final boolean PROPERTIES_NEED_CONVERT;
46     static {
47         boolean isEBCDIC = false;
48         try {
49             String encoding = System.getProperty("file.encoding");
50             if (encoding.contains("EBCDIC")) {
51                 isEBCDIC = true;
52             }
53         } catch (SecurityException e) {
54             // Ignore
55         }
56         PROPERTIES_NEED_CONVERT = isEBCDIC;
57     }
58
59
60     private final File resource;
61     private final String name;
62     private final boolean readOnly;
63     private final Manifest manifest;
64     private final boolean needConvert;
65
66     public FileResource(WebResourceRoot root, String webAppPath,
67             File resource, boolean readOnly, Manifest manifest) {
68         super(root,webAppPath);
69         this.resource = resource;
70
71         if (webAppPath.charAt(webAppPath.length() - 1) == '/') {
72             String realName = resource.getName() + '/';
73             if (webAppPath.endsWith(realName)) {
74                 name = resource.getName();
75             } else {
76                 // This is the root directory of a mounted ResourceSet
77                 // Need to return the mounted name, not the real name
78                 int endOfName = webAppPath.length() - 1;
79                 name = webAppPath.substring(
80                         webAppPath.lastIndexOf('/', endOfName - 1) + 1,
81                         endOfName);
82             }
83         } else {
84             // Must be a file
85             name = resource.getName();
86         }
87
88         this.readOnly = readOnly;
89         this.manifest = manifest;
90         this.needConvert = PROPERTIES_NEED_CONVERT && name.endsWith(".properties");
91     }
92
93     @Override
94     public long getLastModified() {
95         return resource.lastModified();
96     }
97
98     @Override
99     public boolean exists() {
100         return resource.exists();
101     }
102
103     @Override
104     public boolean isVirtual() {
105         return false;
106     }
107
108     @Override
109     public boolean isDirectory() {
110         return resource.isDirectory();
111     }
112
113     @Override
114     public boolean isFile() {
115         return resource.isFile();
116     }
117
118     @Override
119     public boolean delete() {
120         if (readOnly) {
121             return false;
122         }
123         return resource.delete();
124     }
125
126     @Override
127     public String getName() {
128         return name;
129     }
130
131     @Override
132     public long getContentLength() {
133         return getContentLengthInternal(needConvert);
134     }
135
136     private long getContentLengthInternal(boolean convert) {
137         if (convert) {
138             byte[] content = getContent();
139             if (content == null) {
140                 return -1;
141             } else {
142                 return content.length;
143             }
144         }
145
146         if (isDirectory()) {
147             return -1;
148         }
149
150         return resource.length();
151     }
152
153     @Override
154     public String getCanonicalPath() {
155         try {
156             return resource.getCanonicalPath();
157         } catch (IOException ioe) {
158             if (log.isDebugEnabled()) {
159                 log.debug(sm.getString("fileResource.getCanonicalPathFail",
160                         resource.getPath()), ioe);
161             }
162             return null;
163         }
164     }
165
166     @Override
167     public boolean canRead() {
168         return resource.canRead();
169     }
170
171     @Override
172     protected InputStream doGetInputStream() {
173         if (needConvert) {
174             byte[] content = getContent();
175             if (content == null) {
176                 return null;
177             } else {
178                 return new ByteArrayInputStream(content);
179             }
180         }
181         try {
182             return new FileInputStream(resource);
183         } catch (FileNotFoundException fnfe) {
184             // Race condition (file has been deleted) - not an error
185             return null;
186         }
187     }
188
189     @Override
190     public final byte[] getContent() {
191         // Use internal version to avoid loop when needConvert is true
192         long len = getContentLengthInternal(false);
193
194         if (len > Integer.MAX_VALUE) {
195             // Can't create an array that big
196             throw new ArrayIndexOutOfBoundsException(sm.getString(
197                     "abstractResource.getContentTooLarge", getWebappPath(),
198                     Long.valueOf(len)));
199         }
200
201         if (len < 0) {
202             // Content is not applicable here (e.g. is a directory)
203             return null;
204         }
205
206         int size = (int) len;
207         byte[] result = new byte[size];
208
209         int pos = 0;
210         try (InputStream is = new FileInputStream(resource)) {
211             while (pos < size) {
212                 int n = is.read(result, pos, size - pos);
213                 if (n < 0) {
214                     break;
215                 }
216                 pos += n;
217             }
218         } catch (IOException ioe) {
219             if (getLog().isDebugEnabled()) {
220                 getLog().debug(sm.getString("abstractResource.getContentFail",
221                         getWebappPath()), ioe);
222             }
223             return null;
224         }
225
226         if (needConvert) {
227             // Workaround for certain files on platforms that use
228             // EBCDIC encoding, when they are read through FileInputStream.
229             // See commit message of rev.303915 for original details
230             // https://svn.apache.org/viewvc?view=revision&revision=303915
231             String str = new String(result);
232             try {
233                 result = str.getBytes(StandardCharsets.UTF_8);
234             } catch (Exception e) {
235                 result = null;
236             }
237         }
238         return result;
239     }
240
241
242     @Override
243     public long getCreation() {
244         try {
245             BasicFileAttributes attrs = Files.readAttributes(resource.toPath(),
246                     BasicFileAttributes.class);
247             return attrs.creationTime().toMillis();
248         } catch (IOException e) {
249             if (log.isDebugEnabled()) {
250                 log.debug(sm.getString("fileResource.getCreationFail",
251                         resource.getPath()), e);
252             }
253             return 0;
254         }
255     }
256
257     @Override
258     public URL getURL() {
259         if (resource.exists()) {
260             try {
261                 return resource.toURI().toURL();
262             } catch (MalformedURLException e) {
263                 if (log.isDebugEnabled()) {
264                     log.debug(sm.getString("fileResource.getUrlFail",
265                             resource.getPath()), e);
266                 }
267                 return null;
268             }
269         } else {
270             return null;
271         }
272     }
273
274     @Override
275     public URL getCodeBase() {
276         if (getWebappPath().startsWith("/WEB-INF/classes/") && name.endsWith(".class")) {
277             return getWebResourceRoot().getResource("/WEB-INF/classes/").getURL();
278         } else {
279             return getURL();
280         }
281     }
282
283     @Override
284     public Certificate[] getCertificates() {
285         return null;
286     }
287
288     @Override
289     public Manifest getManifest() {
290         return manifest;
291     }
292
293     @Override
294     protected Log getLog() {
295         return log;
296     }
297 }
298