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.markup.html.image; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.List; 022 023import org.apache.wicket.Component; 024import org.apache.wicket.IRequestListener; 025import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; 026import org.apache.wicket.markup.ComponentTag; 027import org.apache.wicket.markup.MarkupStream; 028import org.apache.wicket.markup.html.CrossOrigin; 029import org.apache.wicket.markup.html.WebComponent; 030import org.apache.wicket.markup.html.image.resource.LocalizedImageResource; 031import org.apache.wicket.model.IModel; 032import org.apache.wicket.model.Model; 033import org.apache.wicket.request.mapper.parameter.PageParameters; 034import org.apache.wicket.request.resource.IResource; 035import org.apache.wicket.request.resource.ResourceReference; 036 037/** 038 * An Image component displays localizable image resources. 039 * <p> 040 * For details of how Images load, generate and manage images, see {@link LocalizedImageResource}. 041 * 042 * The first ResourceReference / ImageResource is used for the src attribute within the img tag, all 043 * following are applied to the srcset. If setXValues(String... values) is used the values are set 044 * behind the srcset elements in the order they are given to the setXValues(String... valus) method. 045 * The separated values in the sizes attribute are set with setSizes(String... sizes) 046 * 047 * @see NonCachingImage 048 * 049 * @author Jonathan Locke 050 * @author Tobias Soloschenko 051 */ 052public class Image extends WebComponent implements IRequestListener 053{ 054 private static final long serialVersionUID = 1L; 055 056 /** The image resource this image component references (src attribute) */ 057 private final LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); 058 059 /** The extra image resources this image component references (srcset attribute) */ 060 private final List<LocalizedImageResource> localizedImageResources = new ArrayList<>(); 061 062 /** The x values to be used within the srcset */ 063 private List<String> xValues = null; 064 065 /** The sizes of the responsive images */ 066 private List<String> sizes = null; 067 068 /** 069 * Cross origin settings 070 */ 071 private CrossOrigin crossOrigin = null; 072 073 /** 074 * This constructor can be used if you override {@link #getImageResourceReference()} or 075 * {@link #getImageResource()} 076 * 077 * @param id 078 */ 079 protected Image(final String id) 080 { 081 super(id); 082 } 083 084 /** 085 * Constructs an image from an image resourcereference. That resource reference will bind its 086 * resource to the current SharedResources. 087 * 088 * If you are using non sticky session clustering and the resource reference is pointing to a 089 * Resource that isn't guaranteed to be on every server, for example a dynamic image or 090 * resources that aren't added with a IInitializer at application startup. Then if only that 091 * resource is requested from another server, without the rendering of the page, the image won't 092 * be there and will result in a broken link. 093 * 094 * @param id 095 * See Component 096 * @param resourceReference 097 * The shared image resource used in the src attribute 098 * @param resourceReferences 099 * The shared image resources used in the srcset attribute 100 */ 101 public Image(final String id, final ResourceReference resourceReference, 102 final ResourceReference... resourceReferences) 103 { 104 this(id, resourceReference, null, resourceReferences); 105 } 106 107 /** 108 * Constructs an image from an image resourcereference. That resource reference will bind its 109 * resource to the current SharedResources. 110 * 111 * If you are using non sticky session clustering and the resource reference is pointing to a 112 * Resource that isn't guaranteed to be on every server, for example a dynamic image or 113 * resources that aren't added with a IInitializer at application startup. Then if only that 114 * resource is requested from another server, without the rendering of the page, the image won't 115 * be there and will result in a broken link. 116 * 117 * @param id 118 * See Component 119 * @param resourceReference 120 * The shared image resource used in the src attribute 121 * @param resourceParameters 122 * The resource parameters 123 * @param resourceReferences 124 * The shared image resources used in the srcset attribute 125 */ 126 public Image(final String id, final ResourceReference resourceReference, 127 PageParameters resourceParameters, final ResourceReference... resourceReferences) 128 { 129 super(id); 130 setImageResourceReference(resourceReference, resourceParameters); 131 setImageResourceReferences(resourceParameters, resourceReferences); 132 } 133 134 /** 135 * Constructs an image directly from an image resource. 136 * 137 * This one doesn't have the 'non sticky session clustering' problem that the ResourceReference 138 * constructor has. But this will result in a non 'stable' url and the url will have request 139 * parameters. 140 * 141 * @param id 142 * See Component 143 * 144 * @param imageResource 145 * The image resource used in the src attribute 146 * @param imageResources 147 * The image resource used in the srcset attribute 148 */ 149 public Image(final String id, final IResource imageResource, final IResource... imageResources) 150 { 151 super(id); 152 setImageResource(imageResource); 153 setImageResources(imageResources); 154 } 155 156 /** 157 * @see org.apache.wicket.Component#Component(String, IModel) 158 */ 159 public Image(final String id, final IModel<?> model) 160 { 161 super(id, model); 162 } 163 164 /** 165 * @param id 166 * See Component 167 * @param string 168 * Name of image 169 * @see org.apache.wicket.Component#Component(String, IModel) 170 */ 171 public Image(final String id, final String string) 172 { 173 this(id, new Model<>(string)); 174 } 175 176 @Override 177 public boolean rendersPage() 178 { 179 return false; 180 } 181 182 @Override 183 public void onRequest() 184 { 185 localizedImageResource.onResourceRequested(null); 186 for (LocalizedImageResource localizedImageResource : localizedImageResources) 187 { 188 localizedImageResource.onResourceRequested(null); 189 } 190 } 191 192 /** 193 * @param imageResource 194 * The new ImageResource to set. 195 */ 196 public void setImageResource(final IResource imageResource) 197 { 198 if (imageResource != null) 199 { 200 localizedImageResource.setResource(imageResource); 201 } 202 } 203 204 /** 205 * 206 * @param imageResources 207 * the new ImageResource to set. 208 */ 209 public void setImageResources(final IResource... imageResources) 210 { 211 localizedImageResources.clear(); 212 for (IResource imageResource : imageResources) 213 { 214 LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); 215 localizedImageResource.setResource(imageResource); 216 localizedImageResources.add(localizedImageResource); 217 } 218 } 219 220 /** 221 * @param resourceReference 222 * The shared ImageResource to set. 223 */ 224 public void setImageResourceReference(final ResourceReference resourceReference) 225 { 226 setImageResourceReference(resourceReference, null); 227 } 228 229 /** 230 * @param resourceReference 231 * The resource reference to set. 232 * @param parameters 233 * the parameters to be applied to the localized image resource 234 */ 235 public void setImageResourceReference(final ResourceReference resourceReference, 236 final PageParameters parameters) 237 { 238 if (localizedImageResource != null) 239 { 240 if (parameters != null) 241 { 242 localizedImageResource.setResourceReference(resourceReference, parameters); 243 } 244 else 245 { 246 localizedImageResource.setResourceReference(resourceReference); 247 } 248 } 249 } 250 251 /** 252 * @param parameters 253 * Set the resource parameters for the resource. 254 * @param resourceReferences 255 * The resource references to set. 256 */ 257 public void setImageResourceReferences(final PageParameters parameters, 258 final ResourceReference... resourceReferences) 259 { 260 localizedImageResources.clear(); 261 for (ResourceReference resourceReference : resourceReferences) 262 { 263 LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); 264 if (parameters != null) 265 { 266 localizedImageResource.setResourceReference(resourceReference, parameters); 267 } 268 else 269 { 270 localizedImageResource.setResourceReference(resourceReference); 271 } 272 localizedImageResources.add(localizedImageResource); 273 } 274 } 275 276 /** 277 * @param values 278 * the x values to be used in the srcset 279 */ 280 public void setXValues(String... values) 281 { 282 if (xValues == null) 283 { 284 xValues = new ArrayList<>(); 285 }else{ 286 xValues.clear(); 287 } 288 xValues.addAll(Arrays.asList(values)); 289 } 290 291 /** 292 * Removes all x values from the image src set 293 */ 294 public void removeXValues() 295 { 296 if (xValues != null) 297 { 298 xValues.clear(); 299 } 300 } 301 302 /** 303 * @param sizes 304 * the sizes to be used in the size 305 */ 306 public void setSizes(String... sizes) 307 { 308 if (this.sizes == null) 309 { 310 this.sizes = new ArrayList<>(); 311 }else{ 312 this.sizes.clear(); 313 } 314 this.sizes.addAll(Arrays.asList(sizes)); 315 } 316 317 /** 318 * Removes all sizes values. The corresponding attribute will not be rendered anymore. 319 */ 320 public void removeSizes() 321 { 322 if (sizes != null) 323 { 324 sizes.clear(); 325 } 326 } 327 328 /** 329 * @see org.apache.wicket.Component#setDefaultModel(org.apache.wicket.model.IModel) 330 */ 331 @Override 332 public Component setDefaultModel(IModel<?> model) 333 { 334 // Null out the image resource, so we reload it (otherwise we'll be 335 // stuck with the old model. 336 for (LocalizedImageResource localizedImageResource : localizedImageResources) 337 { 338 localizedImageResource.setResourceReference(null); 339 localizedImageResource.setResource(null); 340 } 341 localizedImageResource.setResourceReference(null); 342 localizedImageResource.setResource(null); 343 return super.setDefaultModel(model); 344 } 345 346 /** 347 * @return Resource returned from subclass 348 */ 349 protected IResource getImageResource() 350 { 351 return localizedImageResource.getResource(); 352 } 353 354 /** 355 * @return ResourceReference returned from subclass 356 */ 357 protected ResourceReference getImageResourceReference() 358 { 359 return localizedImageResource.getResourceReference(); 360 } 361 362 /** 363 * @see org.apache.wicket.Component#initModel() 364 */ 365 @Override 366 protected IModel<?> initModel() 367 { 368 // Images don't support Compound models. They either have a simple 369 // model, explicitly set, or they use their tag's src or value 370 // attribute to determine the image. 371 return null; 372 } 373 374 /** 375 * @see org.apache.wicket.Component#onComponentTag(ComponentTag) 376 */ 377 @Override 378 protected void onComponentTag(final ComponentTag tag) 379 { 380 super.onComponentTag(tag); 381 382 if ("source".equals(tag.getName())) 383 { 384 buildSrcSetAttribute(tag); 385 tag.remove("src"); 386 } 387 else 388 { 389 checkComponentTag(tag, "img"); 390 String srcAttribute = buildSrcAttribute(tag); 391 buildSrcSetAttribute(tag); 392 tag.put("src", srcAttribute); 393 } 394 buildSizesAttribute(tag); 395 396 CrossOrigin crossOrigin = getCrossOrigin(); 397 if (crossOrigin != null && CrossOrigin.NO_CORS != crossOrigin) 398 { 399 tag.put("crossOrigin", crossOrigin.getRealName()); 400 } 401 } 402 403 /** 404 * Builds the srcset attribute if multiple localizedImageResources are found as varargs 405 * 406 * @param tag 407 * the component tag 408 */ 409 protected void buildSrcSetAttribute(final ComponentTag tag) 410 { 411 int srcSetPosition = 0; 412 for (LocalizedImageResource localizedImageResource : localizedImageResources) 413 { 414 localizedImageResource.setSrcAttribute(tag); 415 416 if (shouldAddAntiCacheParameter()) 417 { 418 addAntiCacheParameter(tag); 419 } 420 421 String srcset = tag.getAttribute("srcset"); 422 String xValue = ""; 423 424 // If there are xValues set process them in the applied order to the srcset attribute. 425 if (xValues != null) 426 { 427 xValue = xValues.size() > srcSetPosition && xValues.get(srcSetPosition) != null 428 ? " " + xValues.get(srcSetPosition) : ""; 429 } 430 tag.put("srcset", (srcset != null ? srcset + ", " : "") + tag.getAttribute("src") + 431 xValue); 432 srcSetPosition++; 433 } 434 } 435 436 /** 437 * Builds the src attribute 438 * 439 * @param tag 440 * the component tag 441 * @return the value of the src attribute 442 */ 443 protected String buildSrcAttribute(final ComponentTag tag) 444 { 445 final IResource resource = getImageResource(); 446 if (resource != null) 447 { 448 localizedImageResource.setResource(resource); 449 } 450 final ResourceReference resourceReference = getImageResourceReference(); 451 if (resourceReference != null) 452 { 453 localizedImageResource.setResourceReference(resourceReference); 454 } 455 localizedImageResource.setSrcAttribute(tag); 456 457 if (shouldAddAntiCacheParameter()) 458 { 459 addAntiCacheParameter(tag); 460 } 461 return tag.getAttribute("src"); 462 } 463 464 /** 465 * builds the sizes attribute of the img tag 466 * 467 * @param tag 468 * the component tag 469 */ 470 protected void buildSizesAttribute(final ComponentTag tag) 471 { 472 // if no sizes have been set then don't build the attribute 473 if (sizes == null) 474 { 475 return; 476 } 477 String sizes = ""; 478 for (String size : this.sizes) 479 { 480 sizes += size + ","; 481 } 482 int lastIndexOf = sizes.lastIndexOf(","); 483 if (lastIndexOf != -1) 484 { 485 sizes = sizes.substring(0, lastIndexOf); 486 } 487 if (!sizes.isEmpty()) 488 { 489 tag.put("sizes", sizes); 490 } 491 } 492 493 /** 494 * Adding an image to {@link org.apache.wicket.ajax.AjaxRequestTarget} most of the times mean 495 * that the image has changes and must be re-rendered. 496 * <p> 497 * With this method the user may change this default behavior for some of her images. 498 * </p> 499 * 500 * @return {@code true} to add the anti cache request parameter, {@code false} - otherwise 501 */ 502 protected boolean shouldAddAntiCacheParameter() 503 { 504 return getRequestCycle().find(IPartialPageRequestHandler.class).isPresent(); 505 } 506 507 /** 508 * Adds random noise to the url every request to prevent the browser from caching the image. 509 * 510 * @param tag 511 */ 512 protected void addAntiCacheParameter(final ComponentTag tag) 513 { 514 String url = tag.getAttributes().getString("src"); 515 url = url + (url.contains("?") ? "&" : "?"); 516 url = url + "antiCache=" + System.currentTimeMillis(); 517 518 tag.put("src", url); 519 } 520 521 /** 522 * @see org.apache.wicket.Component#getStatelessHint() 523 */ 524 @Override 525 protected boolean getStatelessHint() 526 { 527 boolean stateless = (getImageResource() == null || getImageResource() == localizedImageResource.getResource()) && 528 localizedImageResource.isStateless(); 529 boolean statelessList = false; 530 for (LocalizedImageResource localizedImageResource : localizedImageResources) 531 { 532 if (localizedImageResource.isStateless()) 533 { 534 statelessList = true; 535 } 536 } 537 return stateless || statelessList; 538 } 539 540 /** 541 * @see org.apache.wicket.Component#onComponentTagBody(MarkupStream, ComponentTag) 542 */ 543 @Override 544 public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) 545 { 546 } 547 548 @Override 549 public boolean canCallListener() 550 { 551 if (isVisibleInHierarchy()) 552 { 553 // when the image data is requested we do not care if this component 554 // is enabled in 555 // hierarchy or not, only that it is visible 556 return true; 557 } 558 else 559 { 560 return super.canCallListener(); 561 } 562 } 563 564 /** 565 * Gets the cross origin settings 566 * 567 * @see #setCrossOrigin(CrossOrigin) 568 * 569 * @return the cross origins settings 570 */ 571 public CrossOrigin getCrossOrigin() 572 { 573 return crossOrigin; 574 } 575 576 /** 577 * Sets the cross origin settings<br> 578 * <br> 579 * 580 * <b>ANONYMOUS</b>: Cross-origin CORS requests for the element will not have the credentials 581 * flag set.<br> 582 * <br> 583 * <b>USE_CREDENTIALS</b>: Cross-origin CORS requests for the element will have the credentials 584 * flag set.<br> 585 * <br> 586 * <b>NO_CORS</b>: The empty string is also a valid keyword, and maps to the Anonymous state. 587 * The attribute's invalid value default is the Anonymous state. The missing value default, used 588 * when the attribute is omitted, is the No CORS state 589 * 590 * @param crossOrigin 591 * the cross origins settings to set 592 */ 593 public void setCrossOrigin(CrossOrigin crossOrigin) 594 { 595 this.crossOrigin = crossOrigin; 596 } 597 598}