001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.core.util.resource; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.URISyntaxException; 022import java.net.URL; 023import java.net.URLConnection; 024import java.time.Instant; 025import java.util.ArrayList; 026import java.util.List; 027import org.apache.wicket.Application; 028import org.apache.wicket.util.io.Connections; 029import org.apache.wicket.util.io.IOUtils; 030import org.apache.wicket.util.lang.Args; 031import org.apache.wicket.util.lang.Bytes; 032import org.apache.wicket.util.lang.Objects; 033import org.apache.wicket.util.resource.AbstractResourceStream; 034import org.apache.wicket.util.resource.IFixedLocationResourceStream; 035import org.apache.wicket.util.resource.ResourceStreamNotFoundException; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039 040/** 041 * UrlResourceStream implements IResource for URLs. 042 * 043 * @see org.apache.wicket.util.resource.IResourceStream 044 * @see org.apache.wicket.util.watch.IModifiable 045 * @author Jonathan Locke 046 * @author Igor Vaynberg 047 */ 048public class UrlResourceStream extends AbstractResourceStream 049 implements 050 IFixedLocationResourceStream 051{ 052 private static final long serialVersionUID = 1L; 053 054 /** Logging. */ 055 private static final Logger log = LoggerFactory.getLogger(UrlResourceStream.class); 056 057 /** 058 * The meta data for this stream. Lazy loaded on demand. 059 */ 060 private transient StreamData streamData; 061 062 /** The URL to this resource. */ 063 private final URL url; 064 065 /** Last known time the stream was last modified. */ 066 private Instant lastModified; 067 068 /** 069 * Meta data class for the stream attributes 070 */ 071 private static class StreamData 072 { 073 private URLConnection connection; 074 075 /** 076 * The streams read from this connection. 077 * Some URLConnection implementations return the same instance of InputStream 078 * every time URLConnection#getInputStream() is called. Other return a new instance 079 * of InputStream. 080 * Here we keep a list of all returned ones and close them in UrlResourceStream#close(). 081 * Even it is the same instance several times we will try to close it quietly several times. 082 */ 083 private List<InputStream> inputStreams; 084 085 /** Length of stream. */ 086 private long contentLength; 087 088 /** Content type for stream. */ 089 private String contentType; 090 091 } 092 093 /** 094 * Construct. 095 * 096 * @param url 097 * URL of resource 098 */ 099 public UrlResourceStream(final URL url) 100 { 101 this.url = Args.notNull(url, "url"); 102 } 103 104 /** 105 * Lazy loads the stream settings on demand 106 * 107 * @param initialize 108 * a flag indicating whether to load the settings 109 * @return the meta data with the stream settings 110 */ 111 private StreamData getData(boolean initialize) 112 { 113 if (streamData == null && initialize) 114 { 115 streamData = new StreamData(); 116 117 try 118 { 119 streamData.connection = url.openConnection(); 120 streamData.contentLength = streamData.connection.getContentLength(); 121 122 // Default implementation "sun.net.www.MimeTable" works on strings with "/" only and 123 // doesn't properly parse paths nor URLs. So providing an absolute URI is compatible 124 // with the default implementation, while the string can't be misinterpreted as path 125 // like has been the case with "URL.getFile" before. That doesn't decode to paths, 126 // results only look similar sometimes. 127 String uriStr = url.toURI().toString(); 128 129 if (Application.exists()) 130 { 131 streamData.contentType = Application.get().getMimeType(uriStr); 132 } 133 else 134 { 135 streamData.contentType = streamData.connection.getContentType(); 136 } 137 138 if (streamData.contentType == null || streamData.contentType.contains("unknown")) 139 { 140 streamData.contentType = URLConnection.getFileNameMap().getContentTypeFor( 141 uriStr); 142 } 143 } 144 catch (IOException | URISyntaxException ex) 145 { 146 throw new IllegalArgumentException("Invalid URL parameter " + url, ex); 147 } 148 } 149 150 return streamData; 151 } 152 153 /** 154 * Closes this resource. 155 * 156 * @throws IOException 157 */ 158 @Override 159 public void close() throws IOException 160 { 161 StreamData data = getData(false); 162 163 if (data != null) 164 { 165 Connections.closeQuietly(data.connection); 166 if (data.inputStreams != null) 167 { 168 for (InputStream is : data.inputStreams) { 169 IOUtils.closeQuietly(is); 170 } 171 } 172 streamData = null; 173 } 174 } 175 176 /** 177 * @return A readable input stream for this resource. 178 * @throws ResourceStreamNotFoundException 179 */ 180 @Override 181 public InputStream getInputStream() throws ResourceStreamNotFoundException 182 { 183 try 184 { 185 StreamData data = getData(true); 186 InputStream is = data.connection.getInputStream(); 187 if (data.inputStreams == null) { 188 data.inputStreams = new ArrayList<>(); 189 } 190 data.inputStreams.add(is); 191 return is; 192 } 193 catch (IOException e) 194 { 195 throw new ResourceStreamNotFoundException("Resource " + url + " could not be opened", e); 196 } 197 } 198 199 /** 200 * @return The URL to this resource (if any) 201 */ 202 public URL getURL() 203 { 204 return url; 205 } 206 207 /** 208 * @see org.apache.wicket.util.watch.IModifiable#lastModifiedTime() 209 * @return The last time this resource was modified 210 */ 211 @Override 212 public Instant lastModifiedTime() 213 { 214 try 215 { 216 // get url modification timestamp 217 final Instant time = Connections.getLastModified(url); 218 219 // if timestamp changed: update content length and last modified date 220 if (Objects.equal(time, lastModified) == false) 221 { 222 lastModified = time; 223 updateContentLength(); 224 } 225 return lastModified; 226 } 227 catch (IOException e) 228 { 229 log.warn("getLastModified() for '{}' failed: {}", url, e.getMessage()); 230 231 // allow modification watcher to detect the problem 232 return null; 233 } 234 } 235 236 private void updateContentLength() throws IOException 237 { 238 StreamData data = getData(false); 239 240 if (data != null) 241 { 242 URLConnection connection = url.openConnection(); 243 try { 244 data.contentLength = connection.getContentLength(); 245 } finally { 246 Connections.close(connection); 247 } 248 } 249 } 250 251 @Override 252 public String toString() 253 { 254 return url.toString(); 255 } 256 257 /** 258 * @return The content type of this resource, such as "image/jpeg" or "text/html" 259 */ 260 @Override 261 public String getContentType() 262 { 263 return getData(true).contentType; 264 } 265 266 @Override 267 public Bytes length() 268 { 269 long length = getData(true).contentLength; 270 271 if (length == -1) 272 { 273 return null; 274 } 275 276 return Bytes.bytes(length); 277 } 278 279 @Override 280 public String locationAsString() 281 { 282 return url.toExternalForm(); 283 } 284}