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}