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.request.resource.caching.version;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.security.MessageDigest;
022import java.security.NoSuchAlgorithmException;
023import java.util.regex.Pattern;
024
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
027import org.apache.wicket.util.io.IOUtils;
028import org.apache.wicket.util.lang.Args;
029import org.apache.wicket.util.lang.Bytes;
030import org.apache.wicket.util.resource.IResourceStream;
031import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
032import org.apache.wicket.util.string.Strings;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036/**
037 * computes the message digest of a {@link org.apache.wicket.request.resource.caching.IStaticCacheableResource} 
038 * and uses it as a version string
039 * <p/>
040 * you can use any message digest algorithm that can be retrieved 
041 * by Java Cryptography Architecture (JCA) on your current platform.
042 * Check <a href="http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA">here</a>
043 * for more information on possible algorithms.
044 * 
045 * @author Peter Ertl
046 * 
047 * @since 1.5
048 */
049public class MessageDigestResourceVersion implements IResourceVersion
050{
051        private static final Logger log = LoggerFactory.getLogger(MessageDigestResourceVersion.class);
052
053        private static final String DEFAULT_ALGORITHM = "MD5";
054        private static final int DEFAULT_BUFFER_BYTES = 8192; // needed for javadoc {@value ..}
055        private static final Bytes DEFAULT_BUFFER_SIZE = Bytes.bytes(DEFAULT_BUFFER_BYTES);
056
057        /**
058         * A valid pattern is a sequence of digits and upper cased English letters A-F
059         */
060        private static final Pattern DIGEST_PATTERN = Pattern.compile("[0-9A-F]+");
061
062        /** 
063         * message digest algorithm for computing hashes 
064         */
065        private final String algorithm;
066
067        /** 
068         * buffer size for computing the digest 
069         */
070        private final Bytes bufferSize;
071
072        /**
073         * create an instance of the message digest 
074         * resource version provider using algorithm {@value #DEFAULT_ALGORITHM}
075         * 
076         * @see #MessageDigestResourceVersion(String) 
077         * @see #MessageDigestResourceVersion(String, org.apache.wicket.util.lang.Bytes)
078         */
079        public MessageDigestResourceVersion()
080        {
081                this(DEFAULT_ALGORITHM, DEFAULT_BUFFER_SIZE);
082        }
083
084        /**
085         * create an instance of the message digest resource version provider 
086         * using the specified algorithm. The algorithm name must be one
087         * that can be retrieved by Java Cryptography Architecture (JCA) 
088         * using {@link MessageDigest#getInstance(String)}. For digest computation
089         * an internal buffer of up to {@value #DEFAULT_BUFFER_BYTES}
090         * bytes will be used.
091         *
092         * @param algorithm
093         *            digest algorithm
094         *
095         * @see #MessageDigestResourceVersion()
096         * @see #MessageDigestResourceVersion(String, org.apache.wicket.util.lang.Bytes)
097         */
098        public MessageDigestResourceVersion(String algorithm)
099        {
100                this(algorithm, DEFAULT_BUFFER_SIZE);
101        }
102
103        /**
104         * create an instance of the message digest resource version provider 
105         * using the specified algorithm. The algorithm name must be one
106         * that can be retrieved by Java Cryptography Architecture (JCA) 
107         * using {@link MessageDigest#getInstance(String)}. For digest computation
108         * an internal buffer with a maximum size specified by parameter 
109         * <code>bufferSize</code> will be used. 
110         *
111         * @param algorithm
112         *            digest algorithm
113         * @param bufferSize
114         *            maximum size for internal buffer            
115         */
116        public MessageDigestResourceVersion(String algorithm, Bytes bufferSize)
117        {
118                this.algorithm = Args.notEmpty(algorithm, "algorithm");
119                this.bufferSize = Args.notNull(bufferSize, "bufferSize");
120        }
121
122        @Override
123        public String getVersion(IStaticCacheableResource resource)
124        {
125                IResourceStream stream = resource.getResourceStream();
126
127                // if resource stream can not be found do not cache
128                if (stream == null)
129                {
130                        return null;
131                }
132
133                try
134                {
135                        final InputStream inputStream = stream.getInputStream();
136
137                        try
138                        {
139                                // get binary hash
140                                final byte[] hash = computeDigest(inputStream);
141
142                                // convert to hexadecimal
143                                return Strings.toHexString(hash);
144                        }
145                        finally
146                        {
147                                IOUtils.close(stream);
148                        }
149                }
150                catch (IOException e)
151                {
152                        log.warn("unable to compute hash for " + resource, e);
153                        return null;
154                }
155                catch (ResourceStreamNotFoundException e)
156                {
157                        log.warn("unable to locate resource for " + resource, e);
158                        return null;
159                }
160        }
161
162        @Override
163        public Pattern getVersionPattern()
164        {
165                return DIGEST_PATTERN;
166        }
167
168        /**
169         * get instance of message digest provider from JCA
170         * 
171         * @return message digest provider
172         */
173        protected MessageDigest getMessageDigest()
174        {
175                try
176                {
177                        return MessageDigest.getInstance(algorithm);
178                }
179                catch (NoSuchAlgorithmException e)
180                {
181                        throw new WicketRuntimeException("message digest " + algorithm + " not found", e);
182                }
183        }
184
185        /**
186         * compute digest for resource stream
187         * 
188         * @param inputStream
189         *            input stream to compute message digest for
190         * 
191         * @return binary message digest
192         * 
193         * @throws IOException
194         */
195        protected byte[] computeDigest(InputStream inputStream) throws IOException
196        {
197                final MessageDigest digest = getMessageDigest();
198
199                // get actual buffer size
200                final int bufferLen = (int)Math.min(Integer.MAX_VALUE, bufferSize.bytes());
201
202                // allocate read buffer
203                final byte[] buf = new byte[bufferLen];
204                int len;
205
206                // read stream and update message digest
207                while ((len = inputStream.read(buf)) != -1)
208                {
209                        digest.update(buf, 0, len);
210                }
211                // finish message digest and return hash
212                return digest.digest();
213        }
214}