/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.markup.html.image; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.IRequestListener; import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.WebComponent; import org.apache.wicket.markup.html.image.resource.LocalizedImageResource; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.IResource; import org.apache.wicket.request.resource.ResourceReference; /** * An Image component displays localizable image resources. * <p> * For details of how Images load, generate and manage images, see {@link LocalizedImageResource}. * * The first ResourceReference / ImageResource is used for the src attribute within the img tag, all * following are applied to the srcset. If setXValues(String... values) is used the values are set * behind the srcset elements in the order they are given to the setXValues(String... valus) method. * The separated values in the sizes attribute are set with setSizes(String... sizes) * * @see NonCachingImage * * @author Jonathan Locke * @author Tobias Soloschenko */ public class Image extends WebComponent implements IRequestListener { private static final long serialVersionUID = 1L; /** * To be used for the crossOrigin attribute * * @see {@link #setCrossOrigin(Cors)} */ public enum Cors { /** * no authentication required */ ANONYMOUS("anonymous"), /** * user credentials required */ USE_CREDENTIALS("user-credentials"), /** * no cross origin */ NO_CORS(""); private final String realName; private Cors(String realName) { this.realName = realName; } /** * Gets the real name for the cors option * * @return the real name */ public String getRealName() { return realName; } } /** The image resource this image component references (src attribute) */ private final LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); /** The extra image resources this image component references (srcset attribute) */ private final List<LocalizedImageResource> localizedImageResources = new ArrayList<>(); /** The x values to be used within the srcset */ private List<String> xValues = null; /** The sizes of the responsive images */ private List<String> sizes = null; /** * Cross origin settings */ private Cors crossOrigin = null; /** * This constructor can be used if you override {@link #getImageResourceReference()} or * {@link #getImageResource()} * * @param id */ protected Image(final String id) { super(id); } /** * Constructs an image from an image resourcereference. That resource reference will bind its * resource to the current SharedResources. * * If you are using non sticky session clustering and the resource reference is pointing to a * Resource that isn't guaranteed to be on every server, for example a dynamic image or * resources that aren't added with a IInitializer at application startup. Then if only that * resource is requested from another server, without the rendering of the page, the image won't * be there and will result in a broken link. * * @param id * See Component * @param resourceReference * The shared image resource used in the src attribute * @param resourceReferences * The shared image resources used in the srcset attribute */ public Image(final String id, final ResourceReference resourceReference, final ResourceReference... resourceReferences) { this(id, resourceReference, null, resourceReferences); } /** * Constructs an image from an image resourcereference. That resource reference will bind its * resource to the current SharedResources. * * If you are using non sticky session clustering and the resource reference is pointing to a * Resource that isn't guaranteed to be on every server, for example a dynamic image or * resources that aren't added with a IInitializer at application startup. Then if only that * resource is requested from another server, without the rendering of the page, the image won't * be there and will result in a broken link. * * @param id * See Component * @param resourceReference * The shared image resource used in the src attribute * @param resourceParameters * The resource parameters * @param resourceReferences * The shared image resources used in the srcset attribute */ public Image(final String id, final ResourceReference resourceReference, PageParameters resourceParameters, final ResourceReference... resourceReferences) { super(id); setImageResourceReference(resourceReference, resourceParameters); setImageResourceReferences(resourceParameters, resourceReferences); } /** * Constructs an image directly from an image resource. * * This one doesn't have the 'non sticky session clustering' problem that the ResourceReference * constructor has. But this will result in a non 'stable' url and the url will have request * parameters. * * @param id * See Component * * @param imageResource * The image resource used in the src attribute * @param imageResources * The image resource used in the srcset attribute */ public Image(final String id, final IResource imageResource, final IResource... imageResources) { super(id); setImageResource(imageResource); setImageResources(imageResources); } /** * @see org.apache.wicket.Component#Component(String, IModel) */ public Image(final String id, final IModel<?> model) { super(id, model); } /** * @param id * See Component * @param string * Name of image * @see org.apache.wicket.Component#Component(String, IModel) */ public Image(final String id, final String string) { this(id, new Model<>(string)); } @Override public boolean rendersPage() { return false; } /** * @see org.apache.wicket.IResourceListener#onResourceRequested() */ @Override public void onRequest() { localizedImageResource.onResourceRequested(null); for (LocalizedImageResource localizedImageResource : localizedImageResources) { localizedImageResource.onResourceRequested(null); } } /** * @param imageResource * The new ImageResource to set. */ public void setImageResource(final IResource imageResource) { if (imageResource != null) { localizedImageResource.setResource(imageResource); } } /** * * @param imageResources * the new ImageResource to set. */ public void setImageResources(final IResource... imageResources) { localizedImageResources.clear(); for (IResource imageResource : imageResources) { LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); localizedImageResource.setResource(imageResource); localizedImageResources.add(localizedImageResource); } } /** * @param resourceReference * The shared ImageResource to set. */ public void setImageResourceReference(final ResourceReference resourceReference) { setImageResourceReference(resourceReference, null); } /** * @param resourceReference * The resource reference to set. * @param parameters * the parameters to be applied to the localized image resource */ public void setImageResourceReference(final ResourceReference resourceReference, final PageParameters parameters) { if (localizedImageResource != null) { if (parameters != null) { localizedImageResource.setResourceReference(resourceReference, parameters); } else { localizedImageResource.setResourceReference(resourceReference); } } } /** * @param parameters * Set the resource parameters for the resource. * @param resourceReferences * The resource references to set. */ public void setImageResourceReferences(final PageParameters parameters, final ResourceReference... resourceReferences) { localizedImageResources.clear(); for (ResourceReference resourceReference : resourceReferences) { LocalizedImageResource localizedImageResource = new LocalizedImageResource(this); if (parameters != null) { localizedImageResource.setResourceReference(resourceReference, parameters); } else { localizedImageResource.setResourceReference(resourceReference); } localizedImageResources.add(localizedImageResource); } } /** * @param values * the x values to be used in the srcset */ public void setXValues(String... values) { if (xValues == null) { xValues = new ArrayList<>(); }else{ xValues.clear(); } xValues.addAll(Arrays.asList(values)); } /** * Removes all x values from the image src set */ public void removeXValues() { if (xValues != null) { xValues.clear(); } } /** * @param sizes * the sizes to be used in the size */ public void setSizes(String... sizes) { if (this.sizes == null) { this.sizes = new ArrayList<>(); }else{ this.sizes.clear(); } this.sizes.addAll(Arrays.asList(sizes)); } /** * Removes all sizes values. The corresponding attribute will not be rendered anymore. */ public void removeSizes() { if (sizes != null) { sizes.clear(); } } /** * @see org.apache.wicket.Component#setDefaultModel(org.apache.wicket.model.IModel) */ @Override public Component setDefaultModel(IModel<?> model) { // Null out the image resource, so we reload it (otherwise we'll be // stuck with the old model. for (LocalizedImageResource localizedImageResource : localizedImageResources) { localizedImageResource.setResourceReference(null); localizedImageResource.setResource(null); } localizedImageResource.setResourceReference(null); localizedImageResource.setResource(null); return super.setDefaultModel(model); } /** * @return Resource returned from subclass */ protected IResource getImageResource() { return localizedImageResource.getResource(); } /** * @return ResourceReference returned from subclass */ protected ResourceReference getImageResourceReference() { return localizedImageResource.getResourceReference(); } /** * @see org.apache.wicket.Component#initModel() */ @Override protected IModel<?> initModel() { // Images don't support Compound models. They either have a simple // model, explicitly set, or they use their tag's src or value // attribute to determine the image. return null; } /** * @see org.apache.wicket.Component#onComponentTag(ComponentTag) */ @Override protected void onComponentTag(final ComponentTag tag) { super.onComponentTag(tag); if ("source".equals(tag.getName())) { buildSrcSetAttribute(tag); tag.remove("src"); } else { checkComponentTag(tag, "img"); String srcAttribute = buildSrcAttribute(tag); buildSrcSetAttribute(tag); tag.put("src", srcAttribute); } buildSizesAttribute(tag); Cors crossOrigin = getCrossOrigin(); if (crossOrigin != null && Cors.NO_CORS != crossOrigin) { tag.put("crossOrigin", crossOrigin.getRealName()); } } /** * Builds the srcset attribute if multiple localizedImageResources are found as varargs * * @param tag * the component tag */ protected void buildSrcSetAttribute(final ComponentTag tag) { int srcSetPosition = 0; for (LocalizedImageResource localizedImageResource : localizedImageResources) { localizedImageResource.setSrcAttribute(tag); if (shouldAddAntiCacheParameter()) { addAntiCacheParameter(tag); } String srcset = tag.getAttribute("srcset"); String xValue = ""; // If there are xValues set process them in the applied order to the srcset attribute. if (xValues != null) { xValue = xValues.size() > srcSetPosition && xValues.get(srcSetPosition) != null ? " " + xValues.get(srcSetPosition) : ""; } tag.put("srcset", (srcset != null ? srcset + ", " : "") + tag.getAttribute("src") + xValue); srcSetPosition++; } } /** * Builds the src attribute * * @param tag * the component tag * @return the value of the src attribute */ protected String buildSrcAttribute(final ComponentTag tag) { final IResource resource = getImageResource(); if (resource != null) { localizedImageResource.setResource(resource); } final ResourceReference resourceReference = getImageResourceReference(); if (resourceReference != null) { localizedImageResource.setResourceReference(resourceReference); } localizedImageResource.setSrcAttribute(tag); if (shouldAddAntiCacheParameter()) { addAntiCacheParameter(tag); } return tag.getAttribute("src"); } /** * builds the sizes attribute of the img tag * * @param tag * the component tag */ protected void buildSizesAttribute(final ComponentTag tag) { // if no sizes have been set then don't build the attribute if (sizes == null) { return; } String sizes = ""; for (String size : this.sizes) { sizes += size + ","; } int lastIndexOf = sizes.lastIndexOf(","); if (lastIndexOf != -1) { sizes = sizes.substring(0, lastIndexOf); } if (!"".equals(sizes)) { tag.put("sizes", sizes); } } /** * Adding an image to {@link org.apache.wicket.ajax.AjaxRequestTarget} most of the times mean * that the image has changes and must be re-rendered. * <p> * With this method the user may change this default behavior for some of her images. * </p> * * @return {@code true} to add the anti cache request parameter, {@code false} - otherwise */ protected boolean shouldAddAntiCacheParameter() { return getRequestCycle().find(IPartialPageRequestHandler.class).isPresent(); } /** * Adds random noise to the url every request to prevent the browser from caching the image. * * @param tag */ protected void addAntiCacheParameter(final ComponentTag tag) { String url = tag.getAttributes().getString("src"); url = url + (url.contains("?") ? "&" : "?"); url = url + "antiCache=" + System.currentTimeMillis(); tag.put("src", url); } /** * @see org.apache.wicket.Component#getStatelessHint() */ @Override protected boolean getStatelessHint() { boolean stateless = (getImageResource() == null || getImageResource() == localizedImageResource.getResource()) && localizedImageResource.isStateless(); boolean statelessList = false; for (LocalizedImageResource localizedImageResource : localizedImageResources) { if (localizedImageResource.isStateless()) { statelessList = true; } } return stateless || statelessList; } /** * @see org.apache.wicket.Component#onComponentTagBody(MarkupStream, ComponentTag) */ @Override public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) { } @Override public boolean canCallListener() { if (isVisibleInHierarchy()) { // when the image data is requested we do not care if this component // is enabled in // hierarchy or not, only that it is visible return true; } else { return super.canCallListener(); } } /** * Gets the cross origin settings * * @see {@link #setCrossOrigin(Cors)} * * @return the cross origins settings */ public Cors getCrossOrigin() { return crossOrigin; } /** * Sets the cross origin settings<br> * <br> * * <b>ANONYMOUS</b>: Cross-origin CORS requests for the element will not have the credentials * flag set.<br> * <br> * <b>USE_CREDENTIALS</b>: Cross-origin CORS requests for the element will have the credentials * flag set.<br> * <br> * <b>NO_CORS</b>: The empty string is also a valid keyword, and maps to the Anonymous state. * The attribute's invalid value default is the Anonymous state. The missing value default, used * when the attribute is omitted, is the No CORS state * * @param crossOrigin * the cross origins settings to set */ public void setCrossOrigin(Cors crossOrigin) { this.crossOrigin = crossOrigin; } }