001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.loader; 029 030import org.opencms.ade.galleries.CmsPreviewService; 031import org.opencms.cache.CmsVfsNameBasedDiskCache; 032import org.opencms.configuration.CmsConfigurationException; 033import org.opencms.configuration.CmsImageCacheConfiguration; 034import org.opencms.configuration.CmsParameterConfiguration; 035import org.opencms.file.CmsFile; 036import org.opencms.file.CmsObject; 037import org.opencms.file.CmsResource; 038import org.opencms.loader.imagecache.CmsImageCacheAccessRenewal; 039import org.opencms.loader.imagecache.CmsImageCacheMaintenanceCleaner; 040import org.opencms.loader.imagecache.CmsImageCacheMaintenanceService; 041import org.opencms.main.CmsEvent; 042import org.opencms.main.CmsException; 043import org.opencms.main.CmsLog; 044import org.opencms.main.I_CmsEventListener; 045import org.opencms.main.OpenCms; 046import org.opencms.scheduler.jobs.CmsImageCacheCleanupJob; 047import org.opencms.security.CmsPermissionSet; 048import org.opencms.util.CmsFileUtil; 049import org.opencms.util.CmsRequestUtil; 050import org.opencms.util.CmsStringUtil; 051 052import java.io.ByteArrayOutputStream; 053import java.io.File; 054import java.io.FileInputStream; 055import java.io.FileOutputStream; 056import java.io.IOException; 057import java.io.OutputStream; 058import java.time.Instant; 059import java.util.Map; 060 061import org.apache.commons.codec.digest.DigestUtils; 062import org.apache.commons.logging.Log; 063 064import jakarta.servlet.http.HttpServletRequest; 065import jakarta.servlet.http.HttpServletResponse; 066 067/** 068 * Loader for images from the OpenCms VSF with integrated image scaling and processing capabilities.<p> 069 * 070 * To scale or process an image, the parameter <code>{@link org.opencms.loader.CmsImageScaler#PARAM_SCALE}</code> 071 * has to be appended to the image URI. The value for the parameter needs to be composed from the <code>SCALE_PARAM</code> 072 * options provided by the constants in the <code>{@link org.opencms.file.types.CmsResourceTypeImage}</code> class.<p> 073 * 074 * For example, to scale an image to exact 800x600 pixel with center fitting and a background color of grey, 075 * the following parameter String can be used: <code>w:800,h:600,t:0,c:c0c0c0</code>.<p> 076 * 077 * @since 6.2.0 078 */ 079public class CmsImageLoader extends CmsDumpLoader implements I_CmsEventListener, I_CmsStaticExportDirectResponseLoader { 080 081 /** Controls max number of threads that are allowed to scale images concurrently. */ 082 public static final String CONFIGURATION_CONCURRENCY = "image.scaling.concurrency"; 083 084 /** The configuration parameter for the OpenCms XML configuration to set the image down scale operation. */ 085 public static final String CONFIGURATION_DOWNSCALE = "image.scaling.downscale"; 086 087 /** The configuration parameter for the OpenCms XML configuration to set the image cache repository. */ 088 public static final String CONFIGURATION_IMAGE_FOLDER = "image.folder"; 089 090 /** The configuration parameter for the OpenCms XML configuration to set the maximum image blur size. */ 091 public static final String CONFIGURATION_MAX_BLUR_SIZE = "image.scaling.maxblursize"; 092 093 /** The configuration parameter for the OpenCms XML configuration to set the maximum image scale size. */ 094 public static final String CONFIGURATION_MAX_SCALE_SIZE = "image.scaling.maxsize"; 095 096 /** The configuration parameter for the OpenCms XML configuration to enable the image scaling. */ 097 public static final String CONFIGURATION_SCALING_ENABLED = "image.scaling.enabled"; 098 099 /** Weak ETag prefix. */ 100 private static final String WEAK_ETAG_PREFIX = "W/"; 101 102 /** Default name for the image cache repository. */ 103 public static final String IMAGE_REPOSITORY_DEFAULT = "/WEB-INF/imagecache/"; 104 105 /** Clear event parameter. */ 106 public static final String PARAM_CLEAR_IMAGES_CACHE = "_IMAGES_CACHE_"; 107 108 /** The id of this loader. */ 109 public static final int RESOURCE_LOADER_ID_IMAGE_LOADER = 2; 110 111 /** The log object for this class. */ 112 protected static final Log LOG = CmsLog.getLog(CmsImageLoader.class); 113 114 /** The (optional) image down scale parameters for image write operations. */ 115 protected static String m_downScaleParams; 116 117 /** Indicates if image scaling is active. */ 118 protected static boolean m_enabled; 119 120 /** The maximum image size (width * height) to apply image blurring when down scaling (setting this to high may cause "out of memory" errors). */ 121 protected static int m_maxBlurSize = CmsImageScaler.SCALE_DEFAULT_MAX_BLUR_SIZE; 122 123 /** The disk cache to use for saving scaled image versions. */ 124 protected static CmsVfsNameBasedDiskCache m_vfsDiskCache; 125 126 /** The image cache repository path, or <code>null</code> for image caches without file system access. */ 127 protected static String m_imageRepositoryPath; 128 129 /** The optional external image cache. */ 130 protected static I_CmsImageCache m_imageCache; 131 132 /** Asynchronous access-triggered renewal for the configured external image cache. */ 133 protected static CmsImageCacheAccessRenewal m_imageCacheAccessRenewal; 134 135 /** The name of the configured image cache repository. */ 136 protected String m_imageRepositoryFolder; 137 138 /** The maximum image size (width or height) to allow when up scaling an image using request parameters. */ 139 protected int m_maxScaleSize = CmsImageScaler.SCALE_DEFAULT_MAX_SIZE; 140 141 /** 142 * Creates a new image loader.<p> 143 */ 144 public CmsImageLoader() { 145 146 super(); 147 } 148 149 /** 150 * Returns the image down scale parameters, 151 * which is set with the {@link #CONFIGURATION_DOWNSCALE} configuration option.<p> 152 * 153 * If no down scale parameters have been set in the configuration, this will return <code>null</code>. 154 * 155 * @return the image down scale parameters 156 */ 157 public static String getDownScaleParams() { 158 159 return m_downScaleParams; 160 } 161 162 /** 163 * Returns the configured external image cache.<p> 164 * 165 * The classic RFS image cache is not represented by an {@link I_CmsImageCache} instance, so this method returns 166 * <code>null</code> for it.<p> 167 * 168 * @return the configured external image cache, or <code>null</code> for the classic RFS image cache 169 */ 170 public static I_CmsImageCache getImageCache() { 171 172 return m_imageCache; 173 } 174 175 /** 176 * Returns the path of the image cache repository folder in the RFS.<p> 177 * 178 * For image cache backends without file system access, this returns <code>null</code>.<p> 179 * 180 * @return the path of the image cache repository folder in the RFS, or <code>null</code> 181 */ 182 public static String getImageRepositoryPath() { 183 184 return m_imageRepositoryPath; 185 } 186 187 /** 188 * The maximum blur size for image re-scale operations, 189 * which is set with the {@link #CONFIGURATION_MAX_BLUR_SIZE} configuration option.<p> 190 * 191 * The default is 2500 * 2500 pixel.<p> 192 * 193 * @return the maximum blur size for image re-scale operations 194 */ 195 public static int getMaxBlurSize() { 196 197 return m_maxBlurSize; 198 } 199 200 /** 201 * Returns <code>true</code> if the image scaling and processing capabilities for the 202 * OpenCms VFS images have been enabled, <code>false</code> if not.<p> 203 * 204 * Image scaling is enabled by setting the loader parameter <code>image.scaling.enabled</code> 205 * to the value <code>true</code> in the configuration file <code>opencms-vfs.xml</code>.<p> 206 * 207 * Enabling image processing in OpenCms may require several additional configuration steps 208 * on the server running OpenCms, especially in UNIX systems. Here it is often required to have an X window server 209 * configured and accessible so that the required Java ImageIO operations work. 210 * Therefore the image scaling capabilities in OpenCms are disabled by default.<p> 211 * 212 * @return <code>true</code> if the image scaling and processing capabilities for the 213 * OpenCms VFS images have been enabled 214 */ 215 public static boolean isEnabled() { 216 217 return m_enabled; 218 } 219 220 /** 221 * Validates the system configuration for an external image cache.<p> 222 * 223 * The cache is closed before a configuration exception is thrown.<p> 224 * 225 * @param imageCache the external image cache 226 * @param configuration the image cache system configuration 227 * @throws CmsConfigurationException if no explicit image cache configuration is present 228 */ 229 static void validateExternalImageCacheConfiguration( 230 I_CmsImageCache imageCache, 231 CmsImageCacheConfiguration configuration) 232 throws CmsConfigurationException { 233 234 if (!configuration.isConfigured()) { 235 CmsConfigurationException configurationException = new CmsConfigurationException( 236 Messages.get().container( 237 Messages.ERR_IMAGE_CACHE_CONFIG_POLICY_REQUIRED_1, 238 imageCache.getClass().getName())); 239 try { 240 imageCache.close(); 241 } catch (Exception e) { 242 configurationException.addSuppressed(e); 243 } 244 throw configurationException; 245 } 246 } 247 248 /** 249 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 250 */ 251 @Override 252 public void addConfigurationParameter(String paramName, String paramValue) { 253 254 if (CmsStringUtil.isNotEmpty(paramName) && CmsStringUtil.isNotEmpty(paramValue)) { 255 if (CONFIGURATION_SCALING_ENABLED.equals(paramName)) { 256 m_enabled = Boolean.valueOf(paramValue).booleanValue(); 257 } 258 if (CONFIGURATION_IMAGE_FOLDER.equals(paramName)) { 259 m_imageRepositoryFolder = paramValue.trim(); 260 } 261 if (CONFIGURATION_MAX_SCALE_SIZE.equals(paramName)) { 262 m_maxScaleSize = CmsStringUtil.getIntValue( 263 paramValue, 264 CmsImageScaler.SCALE_DEFAULT_MAX_SIZE, 265 paramName); 266 } 267 if (CONFIGURATION_MAX_BLUR_SIZE.equals(paramName)) { 268 m_maxBlurSize = CmsStringUtil.getIntValue( 269 paramValue, 270 CmsImageScaler.SCALE_DEFAULT_MAX_BLUR_SIZE, 271 paramName); 272 } 273 if (CONFIGURATION_DOWNSCALE.equals(paramName)) { 274 m_downScaleParams = paramValue.trim(); 275 } 276 277 if (CONFIGURATION_CONCURRENCY.equals(paramName)) { 278 int concurrency = CmsStringUtil.getIntValue(paramValue, CmsImageScaler.DEFAULT_CONCURRENCY, paramName); 279 if (concurrency > 0) { 280 CmsImageScaler.setConcurrency(concurrency); 281 } 282 } 283 } 284 super.addConfigurationParameter(paramName, paramValue); 285 } 286 287 /** 288 * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) 289 */ 290 public void cmsEvent(CmsEvent event) { 291 292 if (event == null) { 293 return; 294 } 295 // only react on the clear caches event 296 int type = event.getType(); 297 if (type != I_CmsEventListener.EVENT_CLEAR_CACHES) { 298 return; 299 } 300 // only react if the clear images cache parameter is set 301 Map<String, ?> data = event.getData(); 302 if (data == null) { 303 return; 304 } 305 Object param = data.get(PARAM_CLEAR_IMAGES_CACHE); 306 if (param == null) { 307 return; 308 } 309 if (m_imageCache != null) { 310 try { 311 float age = param instanceof Number 312 ? ((Number)param).floatValue() 313 : Float.valueOf(String.valueOf(param)).floatValue(); 314 Instant cutoff = Instant.ofEpochMilli(System.currentTimeMillis() - (long)(age * 60f * 60f * 1000f)); 315 CmsImageCacheMaintenanceService service = CmsImageCacheMaintenanceService.create(m_imageCache); 316 int batchSize = 1000; 317 CmsImageCacheMaintenanceCleaner.Result result = CmsImageCacheMaintenanceCleaner.delete( 318 service, 319 cutoff, 320 batchSize, 321 null); 322 if (result.getFailed() > 0) { 323 LOG.warn( 324 "Unable to delete " 325 + result.getFailed() 326 + " configured image cache entries during time-based cleanup."); 327 } 328 } catch (Exception e) { 329 LOG.error("Unable to clean the configured image cache by timestamp.", e); 330 } 331 return; 332 } 333 float age = -1; 334 if (param instanceof String) { 335 age = Float.valueOf((String)param).floatValue(); 336 } else if (param instanceof Number) { 337 age = ((Number)param).floatValue(); 338 } else { 339 return; 340 } 341 CmsImageCacheCleanupJob.cleanImageCache(age); 342 } 343 344 /** 345 * @see org.opencms.loader.I_CmsResourceLoader#destroy() 346 */ 347 @Override 348 public void destroy() { 349 350 m_enabled = false; 351 m_vfsDiskCache = null; 352 m_imageRepositoryPath = null; 353 if (m_imageCacheAccessRenewal != null) { 354 m_imageCacheAccessRenewal.close(); 355 m_imageCacheAccessRenewal = null; 356 } 357 if (m_imageCache != null) { 358 try { 359 m_imageCache.close(); 360 } catch (Exception e) { 361 LOG.warn("Unable to close the configured image cache.", e); 362 } 363 } 364 m_imageCache = null; 365 m_imageRepositoryFolder = null; 366 } 367 368 /** 369 * @see org.opencms.loader.I_CmsResourceLoader#export(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 370 */ 371 @Override 372 public byte[] export(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res) 373 throws IOException, CmsException { 374 375 if (m_enabled && (req != null)) { 376 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 377 if (scaler.isValid()) { 378 if (m_imageCache != null) { 379 String key = ensureImageCacheEntry(cms, resource, scaler, isHeadImageCacheRequest(req)); 380 ByteArrayOutputStream out = new ByteArrayOutputStream(); 381 writeImageCacheEntryTo(cms, resource, scaler, key, out); 382 CmsFile file = (resource instanceof CmsFile) ? (CmsFile)resource : new CmsFile(resource); 383 file.setContents(out.toByteArray()); 384 if (res != null) { 385 loadImageCacheEntry(cms, resource, scaler, key, req, res); 386 } 387 return file.getContents(); 388 } 389 CmsFile file = getScaledImage(cms, resource, scaler); 390 if (res != null) { 391 super.load(cms, file, req, res); 392 } 393 return file.getContents(); 394 } 395 } 396 return super.export(cms, resource, req, res); 397 } 398 399 /** 400 * @see org.opencms.loader.I_CmsStaticExportStreamLoader#exportTo(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, java.io.OutputStream) 401 */ 402 @Override 403 public void exportTo( 404 CmsObject cms, 405 CmsResource resource, 406 HttpServletRequest req, 407 HttpServletResponse res, 408 OutputStream exportOut) 409 throws IOException, CmsException { 410 411 if (m_enabled && (req != null)) { 412 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 413 if (scaler.isValid()) { 414 if (m_imageCache != null) { 415 String key = ensureImageCacheEntry(cms, resource, scaler, isHeadImageCacheRequest(req)); 416 writeImageCacheEntryTo(cms, resource, scaler, key, exportOut); 417 if (res != null) { 418 loadImageCacheEntry(cms, resource, scaler, key, req, res); 419 } 420 return; 421 } 422 File imageCacheFile = getScaledImageFile(cms, resource, scaler); 423 try (FileInputStream in = new FileInputStream(imageCacheFile)) { 424 CmsFileUtil.copy(in, exportOut); 425 } 426 if (res != null) { 427 loadCacheFile(resource, imageCacheFile, req, res); 428 } 429 return; 430 } 431 } 432 super.exportTo(cms, resource, req, res, exportOut); 433 } 434 435 /** 436 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 437 */ 438 @Override 439 public CmsParameterConfiguration getConfiguration() { 440 441 CmsParameterConfiguration result = new CmsParameterConfiguration(); 442 CmsParameterConfiguration config = super.getConfiguration(); 443 if (config != null) { 444 result.putAll(config); 445 } 446 return result; 447 } 448 449 /** 450 * @see org.opencms.loader.I_CmsResourceLoader#getLoaderId() 451 */ 452 @Override 453 public int getLoaderId() { 454 455 return RESOURCE_LOADER_ID_IMAGE_LOADER; 456 } 457 458 /** 459 * Returns the RFS cache file for the image scaler parameters from the request.<p> 460 * 461 * @param cms the current users OpenCms context 462 * @param resource the base VFS resource for the image 463 * @param req the current request 464 * 465 * @return the scaled image cache file, or <code>null</code> if the request contains no valid scaling parameters 466 * 467 * @throws IOException in case of errors accessing the disk based cache 468 * @throws CmsException in case of errors accessing the OpenCms VFS 469 */ 470 public File getScaledImageFile(CmsObject cms, CmsResource resource, HttpServletRequest req) 471 throws IOException, CmsException { 472 473 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 474 if (scaler.isValid()) { 475 return getScaledImageFile(cms, resource, scaler); 476 } 477 return null; 478 } 479 480 /** 481 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 482 */ 483 @Override 484 public void initConfiguration() throws CmsConfigurationException { 485 486 if (CmsStringUtil.isEmpty(m_imageRepositoryFolder)) { 487 m_imageRepositoryFolder = IMAGE_REPOSITORY_DEFAULT; 488 } 489 m_imageCache = null; 490 m_imageRepositoryPath = null; 491 m_vfsDiskCache = new CmsVfsNameBasedDiskCache("", m_imageRepositoryFolder); 492 if (CmsLog.INIT.isInfoEnabled()) { 493 CmsLog.INIT.info( 494 Messages.get().getBundle().key(Messages.INIT_IMAGE_SCALING_ENABLED_1, Boolean.valueOf(m_enabled))); 495 } 496 } 497 498 /** 499 * Initializes the image cache after the complete OpenCms runtime configuration is available.<p> 500 * 501 * @param propertyConfiguration the OpenCms properties 502 * @throws CmsConfigurationException if the image cache can not be initialized 503 */ 504 public void initializeImageCache(CmsParameterConfiguration propertyConfiguration) throws CmsConfigurationException { 505 506 initImageCache(propertyConfiguration); 507 OpenCms.addCmsEventListener(this); 508 if (CmsLog.INIT.isInfoEnabled() && (m_imageRepositoryPath != null)) { 509 CmsLog.INIT.info( 510 Messages.get().getBundle().key(Messages.INIT_IMAGE_REPOSITORY_PATH_1, m_imageRepositoryPath)); 511 } 512 } 513 514 /** 515 * @see org.opencms.loader.I_CmsStoredContentDirectDeliveryLoader#isStoredContentDirectDeliveryEnabled(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 516 */ 517 @Override 518 public boolean isStoredContentDirectDeliveryEnabled( 519 CmsObject cms, 520 CmsResource resource, 521 HttpServletRequest req, 522 HttpServletResponse res) 523 throws CmsException { 524 525 if ((req == null) || !m_enabled) { 526 return true; 527 } 528 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 529 return !scaler.isValid(); 530 } 531 532 /** 533 * @see org.opencms.loader.I_CmsResourceLoader#load(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 534 */ 535 @Override 536 public void load(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res) 537 throws IOException, CmsException { 538 539 if (m_enabled) { 540 // get the scale information from the request 541 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 542 if (scaler.isValid()) { 543 if (m_imageCache != null) { 544 String key = getImageCacheKey(resource, scaler); 545 String etag = getImageCacheETag(key); 546 if (trySendImageCacheNotModified(resource, req, res, etag)) { 547 recordImageCacheAccess(key); 548 return; 549 } 550 key = ensureImageCacheEntry(cms, resource, scaler, isHeadImageCacheRequest(req)); 551 loadImageCacheEntry(cms, resource, scaler, key, req, res, etag); 552 return; 553 } 554 if (canSendLastModifiedHeader(resource, req, res)) { 555 // no image processing required at all 556 return; 557 } 558 File file = getScaledImageFile(cms, resource, scaler); 559 loadCacheFile(resource, file, req, res); 560 } else { 561 super.load(cms, resource, req, res); 562 } 563 } else { 564 // scaling is disabled 565 super.load(cms, resource, req, res); 566 } 567 } 568 569 /** 570 * Loads an image cache file to the response.<p> 571 * 572 * @param resource the original resource 573 * @param file the image cache file 574 * @param req the current request 575 * @param res the current response 576 * 577 * @throws IOException in case writing fails 578 */ 579 public void loadCacheFile(CmsResource resource, File file, HttpServletRequest req, HttpServletResponse res) 580 throws IOException { 581 582 res.setStatus(HttpServletResponse.SC_OK); 583 res.setContentLength((int)file.length()); 584 setDateHeaders(resource, req, res); 585 try (FileInputStream in = new FileInputStream(file)) { 586 CmsFileUtil.copy(in, res.getOutputStream()); 587 } 588 } 589 590 /** 591 * @see org.opencms.loader.I_CmsStaticExportDirectResponseLoader#tryExportDirectResponse(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 592 */ 593 public boolean tryExportDirectResponse( 594 CmsObject cms, 595 CmsResource resource, 596 HttpServletRequest req, 597 HttpServletResponse res) 598 throws IOException, CmsException { 599 600 if ((m_imageCache == null) 601 || !m_enabled 602 || (req == null) 603 || !("GET".equalsIgnoreCase(req.getMethod()) || "HEAD".equalsIgnoreCase(req.getMethod()))) { 604 return false; 605 } 606 if (!cms.hasPermissions(resource, CmsPermissionSet.ACCESS_READ)) { 607 return false; 608 } 609 CmsImageScaler scaler = new CmsImageScaler(req, m_maxScaleSize, m_maxBlurSize); 610 if (!scaler.isValid()) { 611 return false; 612 } 613 String key = getImageCacheKey(resource, scaler); 614 String etag = null; 615 if (res != null) { 616 etag = getImageCacheETag(key); 617 if (trySendImageCacheNotModified(resource, req, res, etag)) { 618 recordImageCacheAccess(key); 619 return true; 620 } 621 } 622 key = ensureImageCacheEntry(cms, resource, scaler, isHeadImageCacheRequest(req)); 623 if (res != null) { 624 loadImageCacheEntry(cms, resource, scaler, key, req, res, etag); 625 } 626 return true; 627 } 628 629 /** 630 * Creates an image cache entry without checking whether it already exists.<p> 631 * 632 * @param cms the CMS context 633 * @param resource the image resource 634 * @param scaler the image scaler 635 * @param key the image cache key 636 * @throws IOException in case scaling fails 637 * @throws CmsException in case VFS or cache access fails 638 */ 639 protected void createImageCacheEntry(CmsObject cms, CmsResource resource, CmsImageScaler scaler, String key) 640 throws IOException, CmsException { 641 642 try { 643 if (scaler.getType() == 8) { 644 // only need the focal point for mode 8 645 scaler.setFocalPoint(CmsPreviewService.readFocalPoint(cms, resource)); 646 } 647 CmsFile file = cms.readFile(resource); 648 ByteArrayOutputStream out = new ByteArrayOutputStream(); 649 scaler.scaleImageTo(file.getContents(), null, resource.getRootPath(), out); 650 m_imageCache.write(key, out.toByteArray()); 651 } catch (IOException | CmsException e) { 652 throw e; 653 } catch (Exception e) { 654 throw new CmsException( 655 Messages.get().container(Messages.ERR_STORED_CONTENT_DELIVERY_2, "image-cache", key), 656 e); 657 } 658 } 659 660 /** 661 * Ensures that an image cache entry exists in the configured image cache.<p> 662 * 663 * @param cms the CMS context 664 * @param resource the image resource 665 * @param scaler the image scaler 666 * 667 * @return the image cache key 668 * 669 * @throws IOException in case scaling fails 670 * @throws CmsException in case VFS access fails 671 */ 672 protected String ensureImageCacheEntry(CmsObject cms, CmsResource resource, CmsImageScaler scaler) 673 throws IOException, CmsException { 674 675 return ensureImageCacheEntry(cms, resource, scaler, false); 676 } 677 678 /** 679 * Returns the image cache key for an image/scaler pair.<p> 680 * 681 * @param resource the image resource 682 * @param scaler the image scaler 683 * 684 * @return the image cache key 685 */ 686 protected String getImageCacheKey(CmsResource resource, CmsImageScaler scaler) { 687 688 String cacheName = m_vfsDiskCache.getCacheName(resource, scaler.toString()); 689 String repositoryPath = m_vfsDiskCache.getRepositoryPath(); 690 if (cacheName.startsWith(repositoryPath)) { 691 return cacheName.substring(repositoryPath.length()); 692 } 693 return cacheName; 694 } 695 696 /** 697 * Returns a scaled version of the given OpenCms VFS image resource.<p> 698 * 699 * All results are cached in disk. 700 * If the scaled version does not exist in the cache, it is created. 701 * Unscaled versions of the images are also stored in the cache.<p> 702 * 703 * @param cms the current users OpenCms context 704 * @param resource the base VFS resource for the image 705 * @param scaler the configured image scaler 706 * 707 * @return a scaled version of the given OpenCms VFS image resource 708 * 709 * @throws IOException in case of errors accessing the disk based cache 710 * @throws CmsException in case of errors accessing the OpenCms VFS 711 */ 712 protected CmsFile getScaledImage(CmsObject cms, CmsResource resource, CmsImageScaler scaler) 713 throws IOException, CmsException { 714 715 File cacheFile = getScaledImageFile(cms, resource, scaler); 716 CmsFile file; 717 if (resource instanceof CmsFile) { 718 // the original file content must be modified (required e.g. for static export) 719 file = (CmsFile)resource; 720 } else { 721 // this is no file, but we don't want to use "upgrade" since we don't need to read the content from the VFS 722 file = new CmsFile(resource); 723 } 724 file.setContents(org.opencms.util.CmsFileUtil.readFile(cacheFile)); 725 return file; 726 } 727 728 /** 729 * Returns the RFS cache file for a scaled version of the given OpenCms VFS image resource.<p> 730 * 731 * @param cms the current users OpenCms context 732 * @param resource the base VFS resource for the image 733 * @param scaler the configured image scaler 734 * 735 * @return the scaled image cache file 736 * 737 * @throws IOException in case of errors accessing the disk based cache 738 * @throws CmsException in case of errors accessing the OpenCms VFS 739 */ 740 protected File getScaledImageFile(CmsObject cms, CmsResource resource, CmsImageScaler scaler) 741 throws IOException, CmsException { 742 743 if ((m_imageCache instanceof CmsFsImageCache) && scaler.isValid()) { 744 String key = ensureImageCacheEntry(cms, resource, scaler); 745 return ((CmsFsImageCache)m_imageCache).getPath(key).toFile(); 746 } 747 String cacheParam = scaler.isValid() ? scaler.toString() : null; 748 String cacheName = m_vfsDiskCache.getCacheName(resource, cacheParam); 749 File cacheFile = m_vfsDiskCache.getCacheFile(cacheName); 750 if (cacheFile == null) { 751 try (FileOutputStream out = new FileOutputStream(m_vfsDiskCache.createCacheFile(cacheName))) { 752 if (scaler.isValid()) { 753 if (scaler.getType() == 8) { 754 // only need the focal point for mode 8 755 scaler.setFocalPoint(CmsPreviewService.readFocalPoint(cms, resource)); 756 } 757 CmsFile file = cms.readFile(resource); 758 scaler.scaleImageTo(file.getContents(), null, resource.getRootPath(), out); 759 } else { 760 cms.readFileContentTo(resource, out); 761 } 762 } 763 cacheFile = m_vfsDiskCache.getCacheFile(cacheName); 764 } 765 return cacheFile; 766 } 767 768 /** 769 * Initializes the classic RFS image cache used when no external image cache backend is selected.<p> 770 */ 771 protected void initDefaultImageCache() { 772 773 if (CmsStringUtil.isEmpty(m_imageRepositoryFolder)) { 774 m_imageRepositoryFolder = IMAGE_REPOSITORY_DEFAULT; 775 } 776 m_imageCache = null; 777 m_imageCacheAccessRenewal = null; 778 m_vfsDiskCache = new CmsVfsNameBasedDiskCache( 779 OpenCms.getSystemInfo().getWebApplicationRfsPath(), 780 m_imageRepositoryFolder); 781 m_imageRepositoryPath = m_vfsDiskCache.getRepositoryPath(); 782 } 783 784 /** 785 * Initializes the optional image cache.<p> 786 */ 787 protected void initImageCache(CmsParameterConfiguration propertyConfiguration) throws CmsConfigurationException { 788 789 m_imageRepositoryPath = null; 790 if (m_imageCacheAccessRenewal != null) { 791 m_imageCacheAccessRenewal.close(); 792 m_imageCacheAccessRenewal = null; 793 } 794 try { 795 I_CmsImageCache imageCache = CmsImageCacheFactory.create(propertyConfiguration); 796 if (imageCache == null) { 797 initDefaultImageCache(); 798 return; 799 } 800 validateExternalImageCacheConfiguration(imageCache, OpenCms.getImageCacheConfiguration()); 801 if (imageCache instanceof CmsS3ImageCache) { 802 m_vfsDiskCache = new CmsVfsNameBasedDiskCache("", ""); 803 m_imageRepositoryPath = null; 804 } else if (imageCache instanceof CmsFsImageCache) { 805 m_vfsDiskCache = new CmsVfsNameBasedDiskCache("", ((CmsFsImageCache)imageCache).getRepositoryPath()); 806 m_imageRepositoryPath = m_vfsDiskCache.getRepositoryPath(); 807 } else { 808 throw new CmsConfigurationException( 809 Messages.get().container(Messages.ERR_IMAGE_CACHE_INIT_1, imageCache.getClass().getName())); 810 } 811 m_imageCache = imageCache; 812 m_imageCacheAccessRenewal = CmsImageCacheAccessRenewal.create( 813 imageCache, 814 OpenCms.getImageCacheConfiguration()); 815 } catch (CmsConfigurationException e) { 816 m_imageCache = null; 817 m_imageRepositoryPath = null; 818 throw e; 819 } catch (Exception e) { 820 m_imageCache = null; 821 m_imageRepositoryPath = null; 822 throw new CmsConfigurationException( 823 Messages.get().container(Messages.ERR_IMAGE_CACHE_INIT_1, "opencms.properties"), 824 e); 825 } 826 } 827 828 /** 829 * Loads an image cache entry from the configured storage to the response.<p> 830 * 831 * @param resource the original resource 832 * @param key the image cache key 833 * @param req the request 834 * @param res the response 835 * 836 * @throws IOException in case writing fails 837 * @throws CmsException in case store access fails 838 */ 839 protected void loadImageCacheEntry( 840 CmsResource resource, 841 String key, 842 HttpServletRequest req, 843 HttpServletResponse res) 844 throws IOException, CmsException { 845 846 String etag = getImageCacheETag(key); 847 if (trySendImageCacheNotModified(resource, req, res, etag)) { 848 recordImageCacheAccess(key); 849 return; 850 } 851 loadImageCacheEntry(resource, key, req, res, etag); 852 } 853 854 /** 855 * Ensures that an image cache entry exists, optionally bypassing locally cached positive metadata.<p> 856 * 857 * @param cms the CMS context 858 * @param resource the image resource 859 * @param scaler the image scaler 860 * @param authoritative whether to perform an authoritative existence check 861 * @return the image cache key 862 * @throws IOException in case scaling fails 863 * @throws CmsException in case VFS or cache access fails 864 */ 865 private String ensureImageCacheEntry( 866 CmsObject cms, 867 CmsResource resource, 868 CmsImageScaler scaler, 869 boolean authoritative) 870 throws IOException, CmsException { 871 872 String key = getImageCacheKey(resource, scaler); 873 try { 874 boolean exists = authoritative ? m_imageCache.existsAuthoritatively(key) : m_imageCache.exists(key); 875 if (!exists) { 876 createImageCacheEntry(cms, resource, scaler, key); 877 } 878 return key; 879 } catch (IOException | CmsException e) { 880 throw e; 881 } catch (Exception e) { 882 throw new CmsException( 883 Messages.get().container(Messages.ERR_STORED_CONTENT_DELIVERY_2, "image-cache", key), 884 e); 885 } 886 } 887 888 /** 889 * Returns the ETag for an image cache entry.<p> 890 * 891 * @param key the image cache key 892 * @return the ETag 893 */ 894 private String getImageCacheETag(String key) { 895 896 return "\"" + DigestUtils.sha256Hex(key) + "\""; 897 } 898 899 /** 900 * Returns if the given value is an entity tag.<p> 901 * 902 * @param value the value 903 * @return <code>true</code> if the value is an entity tag 904 */ 905 private boolean isEntityTag(String value) { 906 907 String tag = value.trim(); 908 return tag.startsWith("\"") || tag.startsWith(WEAK_ETAG_PREFIX + "\""); 909 } 910 911 /** Returns whether the request uses the HEAD method. */ 912 private boolean isHeadImageCacheRequest(HttpServletRequest req) { 913 914 return "HEAD".equalsIgnoreCase(req.getMethod()); 915 } 916 917 /** 918 * Returns if the image cache entry was not modified since the client request.<p> 919 * 920 * @param resource the resource 921 * @param req the request 922 * @param res the response 923 * @param etag the image cache entry ETag 924 * @return if the image cache entry was not modified 925 */ 926 private boolean isImageCacheNotModified( 927 CmsResource resource, 928 HttpServletRequest req, 929 HttpServletResponse res, 930 String etag) { 931 932 boolean hasIfNoneMatch = req.getHeader(CmsRequestUtil.HEADER_IF_NONE_MATCH) != null; 933 boolean notModified = hasIfNoneMatch 934 ? matchesImageCacheIfNoneMatch(req, etag) 935 : org.opencms.flex.CmsFlexController.isNotModifiedSince(req, resource.getDateLastModified()); 936 if (resource.getState().isUnchanged() 937 && !org.opencms.workplace.CmsWorkplaceManager.isWorkplaceUser(req) 938 && notModified) { 939 long now = System.currentTimeMillis(); 940 if ((resource.getDateReleased() < now) && (resource.getDateExpired() > now)) { 941 setImageCacheHeaders(resource, req, res, etag); 942 res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 943 return true; 944 } 945 } 946 return false; 947 } 948 949 /** 950 * Loads an image cache entry and regenerates it once if an external cache reports that it disappeared.<p> 951 * 952 * @param cms the CMS context 953 * @param resource the original resource 954 * @param scaler the image scaler 955 * @param key the image cache key 956 * @param req the request 957 * @param res the response 958 * @throws IOException in case writing fails 959 * @throws CmsException in case VFS or cache access fails 960 */ 961 private void loadImageCacheEntry( 962 CmsObject cms, 963 CmsResource resource, 964 CmsImageScaler scaler, 965 String key, 966 HttpServletRequest req, 967 HttpServletResponse res) 968 throws IOException, CmsException { 969 970 String etag = getImageCacheETag(key); 971 if (trySendImageCacheNotModified(resource, req, res, etag)) { 972 recordImageCacheAccess(key); 973 return; 974 } 975 loadImageCacheEntry(cms, resource, scaler, key, req, res, etag); 976 } 977 978 /** 979 * Loads an image cache entry after conditional request handling and retries once after regeneration.<p> 980 * 981 * @param cms the CMS context 982 * @param resource the original resource 983 * @param scaler the image scaler 984 * @param key the image cache key 985 * @param req the request 986 * @param res the response 987 * @param etag the image cache entry ETag 988 * @throws IOException in case writing fails 989 * @throws CmsException in case VFS or cache access fails 990 */ 991 private void loadImageCacheEntry( 992 CmsObject cms, 993 CmsResource resource, 994 CmsImageScaler scaler, 995 String key, 996 HttpServletRequest req, 997 HttpServletResponse res, 998 String etag) 999 throws IOException, CmsException { 1000 1001 try { 1002 loadImageCacheEntry(resource, key, req, res, etag); 1003 } catch (CmsImageCacheEntryNotFoundException e) { 1004 if (res.isCommitted()) { 1005 throw e; 1006 } 1007 res.resetBuffer(); 1008 if (LOG.isDebugEnabled()) { 1009 LOG.debug("Regenerating missing image cache entry '" + key + "'."); 1010 } 1011 createImageCacheEntry(cms, resource, scaler, key); 1012 loadImageCacheEntry(resource, key, req, res, etag); 1013 } 1014 } 1015 1016 /** 1017 * Loads an image cache entry from the configured storage after conditional request handling.<p> 1018 * 1019 * @param resource the original resource 1020 * @param key the image cache key 1021 * @param req the request 1022 * @param res the response 1023 * @param etag the image cache entry ETag 1024 * 1025 * @throws IOException in case writing fails 1026 * @throws CmsException in case store access fails 1027 */ 1028 private void loadImageCacheEntry( 1029 CmsResource resource, 1030 String key, 1031 HttpServletRequest req, 1032 HttpServletResponse res, 1033 String etag) 1034 throws IOException, CmsException { 1035 1036 try { 1037 long length = m_imageCache.getLength(key); 1038 String rangeHeader = req.getHeader(CmsRequestUtil.HEADER_RANGE); 1039 boolean rangeRequest = (rangeHeader != null) 1040 && "GET".equalsIgnoreCase(req.getMethod()) 1041 && m_imageCache.supportsRangeDelivery() 1042 && matchesImageCacheIfRange(req, resource, etag); 1043 if (rangeRequest) { 1044 CmsByteRange range = CmsByteRange.parse(rangeHeader, length); 1045 if (range.isInvalid()) { 1046 res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 1047 res.setHeader(CmsRequestUtil.HEADER_CONTENT_RANGE, CmsByteRange.RANGE_UNIT_BYTES + " */" + length); 1048 res.setHeader(CmsRequestUtil.HEADER_ETAG, etag); 1049 return; 1050 } 1051 if (!range.isIgnored()) { 1052 res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); 1053 setContentLength(res, range.m_length); 1054 res.setHeader( 1055 CmsRequestUtil.HEADER_CONTENT_RANGE, 1056 CmsByteRange.RANGE_UNIT_BYTES 1057 + " " 1058 + range.m_start 1059 + "-" 1060 + (range.m_start + range.m_length - 1) 1061 + "/" 1062 + length); 1063 setImageCacheHeaders(resource, req, res, etag); 1064 if (!"HEAD".equalsIgnoreCase(req.getMethod())) { 1065 m_imageCache.writeRangeTo(key, range.m_start, range.m_length, res.getOutputStream()); 1066 recordImageCacheAccess(key); 1067 } 1068 return; 1069 } 1070 } 1071 res.setStatus(HttpServletResponse.SC_OK); 1072 setContentLength(res, length); 1073 setImageCacheHeaders(resource, req, res, etag); 1074 if (!"HEAD".equalsIgnoreCase(req.getMethod())) { 1075 writeImageCacheEntryTo(key, res.getOutputStream()); 1076 } 1077 } catch (IOException | CmsException e) { 1078 throw e; 1079 } catch (Exception e) { 1080 throw new CmsException( 1081 Messages.get().container(Messages.ERR_STORED_CONTENT_DELIVERY_2, "image-cache", key), 1082 e); 1083 } 1084 } 1085 1086 /** 1087 * Returns if the If-None-Match header matches the image cache entry ETag.<p> 1088 * 1089 * @param req the request 1090 * @param etag the image cache entry ETag 1091 * @return if the entity tag matches 1092 */ 1093 private boolean matchesImageCacheIfNoneMatch(HttpServletRequest req, String etag) { 1094 1095 String ifNoneMatch = req.getHeader(CmsRequestUtil.HEADER_IF_NONE_MATCH); 1096 if (ifNoneMatch == null) { 1097 return false; 1098 } 1099 String imageCacheETag = stripWeakPrefix(etag); 1100 String[] tags = ifNoneMatch.split(","); 1101 for (int i = 0; i < tags.length; i++) { 1102 String tag = stripWeakPrefix(tags[i].trim()); 1103 if ("*".equals(tag) || imageCacheETag.equals(tag)) { 1104 return true; 1105 } 1106 } 1107 return false; 1108 } 1109 1110 /** 1111 * Checks the If-Range header for image cache delivery.<p> 1112 * 1113 * @param req the request 1114 * @param resource the resource 1115 * @param etag the image cache entry ETag 1116 * @return if a range request should be processed 1117 */ 1118 private boolean matchesImageCacheIfRange(HttpServletRequest req, CmsResource resource, String etag) { 1119 1120 String ifRange = req.getHeader(CmsRequestUtil.HEADER_IF_RANGE); 1121 if (ifRange == null) { 1122 return true; 1123 } 1124 if (isEntityTag(ifRange)) { 1125 return etag.equals(ifRange.trim()); 1126 } 1127 try { 1128 long ifRangeDate = req.getDateHeader(CmsRequestUtil.HEADER_IF_RANGE); 1129 return (ifRangeDate >= 0) && ((resource.getDateLastModified() / 1000) * 1000 == ifRangeDate); 1130 } catch (IllegalArgumentException e) { 1131 return false; 1132 } 1133 } 1134 1135 /** Records a successful external image cache access without delaying the response. */ 1136 private void recordImageCacheAccess(String key) { 1137 1138 CmsImageCacheAccessRenewal accessRenewal = m_imageCacheAccessRenewal; 1139 if (accessRenewal != null) { 1140 accessRenewal.recordAccess(key); 1141 } 1142 } 1143 1144 /** 1145 * Sets the content length header.<p> 1146 * 1147 * @param res the response 1148 * @param length the content length 1149 */ 1150 private void setContentLength(HttpServletResponse res, long length) { 1151 1152 if (length <= Integer.MAX_VALUE) { 1153 res.setContentLength((int)length); 1154 } else { 1155 res.setHeader(CmsRequestUtil.HEADER_CONTENT_LENGTH, String.valueOf(length)); 1156 } 1157 } 1158 1159 /** 1160 * Sets date and cache headers for a loaded image.<p> 1161 * 1162 * @param resource the original resource 1163 * @param req the current request 1164 * @param res the current response 1165 */ 1166 private void setDateHeaders(CmsResource resource, HttpServletRequest req, HttpServletResponse res) { 1167 1168 if (org.opencms.workplace.CmsWorkplaceManager.isWorkplaceUser(req)) { 1169 res.setDateHeader(org.opencms.util.CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis()); 1170 org.opencms.util.CmsRequestUtil.setNoCacheHeaders(res); 1171 } else { 1172 res.setDateHeader(org.opencms.util.CmsRequestUtil.HEADER_LAST_MODIFIED, resource.getDateLastModified()); 1173 if (!res.containsHeader(org.opencms.util.CmsRequestUtil.HEADER_CACHE_CONTROL)) { 1174 long expireTime = resource.getDateExpired(); 1175 if (expireTime == CmsResource.DATE_EXPIRED_DEFAULT) { 1176 expireTime--; 1177 } 1178 org.opencms.flex.CmsFlexController.setDateExpiresHeader(res, expireTime, getClientCacheMaxAge()); 1179 } 1180 } 1181 } 1182 1183 /** 1184 * Sets HTTP headers for an image cache entry.<p> 1185 * 1186 * @param resource the original resource 1187 * @param req the current request 1188 * @param res the current response 1189 * @param etag the image cache entry ETag 1190 */ 1191 private void setImageCacheHeaders( 1192 CmsResource resource, 1193 HttpServletRequest req, 1194 HttpServletResponse res, 1195 String etag) { 1196 1197 res.setHeader(CmsRequestUtil.HEADER_ETAG, etag); 1198 setDateHeaders(resource, req, res); 1199 } 1200 1201 /** 1202 * Removes a weak entity tag prefix from the given value.<p> 1203 * 1204 * @param value the value 1205 * @return the value without weak prefix 1206 */ 1207 private String stripWeakPrefix(String value) { 1208 1209 return value.startsWith(WEAK_ETAG_PREFIX) ? value.substring(WEAK_ETAG_PREFIX.length()) : value; 1210 } 1211 1212 /** 1213 * Tries to answer a conditional image cache request without accessing the configured image cache storage.<p> 1214 * 1215 * @param resource the original resource 1216 * @param req the request 1217 * @param res the response 1218 * @param etag the image cache entry ETag 1219 * @return if the response was answered with 304 1220 */ 1221 private boolean trySendImageCacheNotModified( 1222 CmsResource resource, 1223 HttpServletRequest req, 1224 HttpServletResponse res, 1225 String etag) { 1226 1227 if (m_imageCache.supportsRangeDelivery()) { 1228 res.setHeader(CmsRequestUtil.HEADER_ACCEPT_RANGES, CmsByteRange.RANGE_UNIT_BYTES); 1229 } 1230 return isImageCacheNotModified(resource, req, res, etag); 1231 } 1232 1233 /** 1234 * Writes an image cache entry and regenerates it once if it disappeared from an external cache.<p> 1235 * 1236 * @param cms the CMS context 1237 * @param resource the original resource 1238 * @param scaler the image scaler 1239 * @param key the image cache key 1240 * @param out the output stream 1241 * @throws IOException in case writing fails 1242 * @throws CmsException in case VFS or cache access fails 1243 */ 1244 private void writeImageCacheEntryTo( 1245 CmsObject cms, 1246 CmsResource resource, 1247 CmsImageScaler scaler, 1248 String key, 1249 OutputStream out) 1250 throws IOException, CmsException { 1251 1252 try { 1253 writeImageCacheEntryTo(key, out); 1254 } catch (CmsImageCacheEntryNotFoundException e) { 1255 if (LOG.isDebugEnabled()) { 1256 LOG.debug("Regenerating missing image cache entry '" + key + "'."); 1257 } 1258 createImageCacheEntry(cms, resource, scaler, key); 1259 writeImageCacheEntryTo(key, out); 1260 } 1261 } 1262 1263 /** 1264 * Writes an image cache entry to an output stream.<p> 1265 * 1266 * @param key the image cache key 1267 * @param out the output stream 1268 * 1269 * @throws IOException in case writing fails 1270 * @throws CmsException in case store access fails 1271 */ 1272 private void writeImageCacheEntryTo(String key, OutputStream out) throws IOException, CmsException { 1273 1274 try { 1275 m_imageCache.writeTo(key, out); 1276 recordImageCacheAccess(key); 1277 } catch (IOException | CmsException e) { 1278 throw e; 1279 } catch (Exception e) { 1280 throw new CmsException( 1281 Messages.get().container(Messages.ERR_STORED_CONTENT_DELIVERY_2, "image-cache", key), 1282 e); 1283 } 1284 } 1285}