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.resource.bundles; 018 019import java.io.ByteArrayInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.Serializable; 023import java.time.Instant; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.MissingResourceException; 027 028import jakarta.servlet.http.HttpServletResponse; 029 030import org.apache.wicket.Application; 031import org.apache.wicket.markup.head.IReferenceHeaderItem; 032import org.apache.wicket.request.resource.AbstractResource; 033import org.apache.wicket.request.resource.IResource; 034import org.apache.wicket.request.resource.ResourceReference; 035import org.apache.wicket.request.resource.caching.IStaticCacheableResource; 036import org.apache.wicket.resource.ITextResourceCompressor; 037import org.apache.wicket.util.io.ByteArrayOutputStream; 038import org.apache.wicket.util.io.IOUtils; 039import org.apache.wicket.util.lang.Args; 040import org.apache.wicket.util.lang.Bytes; 041import org.apache.wicket.util.lang.Classes; 042import org.apache.wicket.util.resource.AbstractResourceStream; 043import org.apache.wicket.util.resource.IResourceStream; 044import org.apache.wicket.util.resource.ResourceStreamNotFoundException; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047 048/** 049 * A {@linkplain IResource resource} that concatenates several resources into one download. This 050 * resource can only bundle {@link IStaticCacheableResource}s. The content type of the resource will 051 * be that of the first resource that specifies its content type. 052 * 053 * @author papegaaij 054 */ 055public class ConcatBundleResource extends AbstractResource implements IStaticCacheableResource 056{ 057 private static final Logger log = LoggerFactory.getLogger(ConcatBundleResource.class); 058 059 private static final long serialVersionUID = 1L; 060 061 private final List<? extends IReferenceHeaderItem> providedResources; 062 063 private boolean cachingEnabled; 064 065 /** 066 * An optional compressor that will be used to compress the bundle resources 067 */ 068 private ITextResourceCompressor compressor; 069 070 /** 071 * Construct. 072 * 073 * @param providedResources 074 */ 075 public ConcatBundleResource(List<? extends IReferenceHeaderItem> providedResources) 076 { 077 this.providedResources = Args.notNull(providedResources, "providedResources"); 078 cachingEnabled = true; 079 } 080 081 @Override 082 protected ResourceResponse newResourceResponse(Attributes attributes) 083 { 084 final ResourceResponse resourceResponse = new ResourceResponse(); 085 086 if (resourceResponse.dataNeedsToBeWritten(attributes)) 087 { 088 try 089 { 090 List<IResourceStream> resources = collectResourceStreams(); 091 if (resources == null) 092 return sendResourceError(resourceResponse, HttpServletResponse.SC_NOT_FOUND, 093 "Unable to find resource"); 094 095 resourceResponse.setContentType(findContentType(resources)); 096 097 // add Last-Modified header (to support HEAD requests and If-Modified-Since) 098 final Instant lastModified = findLastModified(resources); 099 100 if (lastModified != null) 101 resourceResponse.setLastModified(lastModified); 102 103 // read resource data 104 final byte[] bytes = readAllResources(resources); 105 106 // send Content-Length header 107 resourceResponse.setContentLength(bytes.length); 108 109 // send response body with resource data 110 resourceResponse.setWriteCallback(new WriteCallback() 111 { 112 @Override 113 public void writeData(Attributes attributes) 114 { 115 attributes.getResponse().write(bytes); 116 } 117 }); 118 } 119 catch (IOException e) 120 { 121 log.debug(e.getMessage(), e); 122 return sendResourceError(resourceResponse, 500, "Unable to read resource stream"); 123 } 124 catch (ResourceStreamNotFoundException e) 125 { 126 log.debug(e.getMessage(), e); 127 return sendResourceError(resourceResponse, 500, "Unable to open resource stream"); 128 } 129 } 130 131 return resourceResponse; 132 } 133 134 private List<IResourceStream> collectResourceStreams() 135 { 136 List<IResourceStream> ret = new ArrayList<>(providedResources.size()); 137 for (IReferenceHeaderItem curItem : providedResources) 138 { 139 IResourceStream stream = ((IStaticCacheableResource)curItem.getReference() 140 .getResource()).getResourceStream(); 141 if (stream == null) 142 { 143 reportError(curItem.getReference(), "Cannot get resource stream for "); 144 return null; 145 } 146 147 ret.add(stream); 148 } 149 return ret; 150 } 151 152 protected String findContentType(List<IResourceStream> resources) 153 { 154 for (IResourceStream curStream : resources) 155 if (curStream.getContentType() != null) 156 return curStream.getContentType(); 157 return null; 158 } 159 160 protected Instant findLastModified(List<IResourceStream> resources) 161 { 162 Instant ret = null; 163 for (IResourceStream curStream : resources) 164 { 165 Instant curLastModified = curStream.lastModifiedTime(); 166 if (ret == null || curLastModified.isAfter(ret)) 167 ret = curLastModified; 168 } 169 return ret; 170 } 171 172 protected byte[] readAllResources(List<IResourceStream> resources) throws IOException, 173 ResourceStreamNotFoundException 174 { 175 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { 176 for (IResourceStream curStream : resources) { 177 IOUtils.copy(curStream.getInputStream(), output); 178 } 179 180 return output.toByteArray(); 181 } 182 } 183 184 private ResourceResponse sendResourceError(ResourceResponse resourceResponse, int errorCode, 185 String errorMessage) 186 { 187 if (log.isWarnEnabled()) 188 { 189 String msg = String.format("Bundled resource: %s (status=%d)", errorMessage, errorCode); 190 log.warn(msg); 191 } 192 193 resourceResponse.setError(errorCode, errorMessage); 194 return resourceResponse; 195 } 196 197 @Override 198 public boolean isCachingEnabled() 199 { 200 return cachingEnabled; 201 } 202 203 public void setCachingEnabled(final boolean enabled) 204 { 205 cachingEnabled = enabled; 206 } 207 208 @Override 209 public Serializable getCacheKey() 210 { 211 ArrayList<Serializable> key = new ArrayList<>(providedResources.size()); 212 for (IReferenceHeaderItem curItem : providedResources) 213 { 214 Serializable curKey = ((IStaticCacheableResource)curItem.getReference().getResource()).getCacheKey(); 215 if (curKey == null) 216 { 217 reportError(curItem.getReference(), "Unable to get cache key for "); 218 return null; 219 } 220 key.add(curKey); 221 } 222 return key; 223 } 224 225 /** 226 * If a bundle resource is missing then throws a {@link MissingResourceException} if 227 * {@link org.apache.wicket.settings.ResourceSettings#getThrowExceptionOnMissingResource()} 228 * says so, or logs a warning message if the logging level allows 229 * @param reference 230 * The resource reference to the missing resource 231 * @param prefix 232 * The error message prefix 233 */ 234 private void reportError(ResourceReference reference, String prefix) 235 { 236 String scope = Classes.name(reference.getScope()); 237 String name = reference.getName(); 238 String message = prefix + reference.toString(); 239 240 if (getThrowExceptionOnMissingResource()) 241 { 242 throw new MissingResourceException(message, scope, name); 243 } 244 else if (log.isWarnEnabled()) 245 { 246 log.warn(message); 247 } 248 } 249 250 @Override 251 public IResourceStream getResourceStream() 252 { 253 List<IResourceStream> streams = collectResourceStreams(); 254 255 if (streams == null) 256 { 257 return null; 258 } 259 260 final String contentType = findContentType(streams); 261 final Instant lastModified = findLastModified(streams); 262 AbstractResourceStream ret = new AbstractResourceStream() 263 { 264 private static final long serialVersionUID = 1L; 265 266 private byte[] bytes; 267 268 private ByteArrayInputStream inputStream; 269 270 private byte[] getBytes() { 271 if (bytes == null) { 272 try 273 { 274 bytes = readAllResources(streams); 275 } 276 catch (IOException e) 277 { 278 return null; 279 } 280 catch (ResourceStreamNotFoundException e) 281 { 282 return null; 283 } 284 } 285 286 return bytes; 287 } 288 289 @Override 290 public InputStream getInputStream() throws ResourceStreamNotFoundException 291 { 292 if (inputStream == null) { 293 inputStream = new ByteArrayInputStream(getBytes()); 294 } 295 296 return inputStream; 297 } 298 299 @Override 300 public Bytes length() 301 { 302 return Bytes.bytes(getBytes().length); 303 } 304 305 @Override 306 public String getContentType() 307 { 308 return contentType; 309 } 310 311 @Override 312 public Instant lastModifiedTime() 313 { 314 return lastModified; 315 } 316 317 @Override 318 public void close() throws IOException 319 { 320 if (inputStream != null) { 321 inputStream.close(); 322 } 323 } 324 }; 325 return ret; 326 } 327 328 public void setCompressor(ITextResourceCompressor compressor) 329 { 330 this.compressor = compressor; 331 } 332 333 public ITextResourceCompressor getCompressor() 334 { 335 return compressor; 336 } 337 338 /** 339 * @return the result of {@link org.apache.wicket.settings.ResourceSettings#getThrowExceptionOnMissingResource()} 340 */ 341 protected boolean getThrowExceptionOnMissingResource() 342 { 343 return Application.get().getResourceSettings().getThrowExceptionOnMissingResource(); 344 } 345}