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}