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.resource;
018
019import java.util.Locale;
020
021import org.apache.wicket.Application;
022import org.apache.wicket.Component;
023import org.apache.wicket.IResourceFactory;
024import org.apache.wicket.MarkupContainer;
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.markup.ComponentTag;
027import org.apache.wicket.markup.html.border.Border;
028import org.apache.wicket.request.cycle.RequestCycle;
029import org.apache.wicket.request.mapper.parameter.PageParameters;
030import org.apache.wicket.request.resource.IResource;
031import org.apache.wicket.request.resource.IResource.Attributes;
032import org.apache.wicket.request.resource.PackageResourceReference;
033import org.apache.wicket.request.resource.ResourceReference;
034import org.apache.wicket.util.io.IClusterable;
035import org.apache.wicket.util.lang.Objects;
036import org.apache.wicket.util.parse.metapattern.Group;
037import org.apache.wicket.util.parse.metapattern.MetaPattern;
038import org.apache.wicket.util.parse.metapattern.OptionalMetaPattern;
039import org.apache.wicket.util.parse.metapattern.parsers.MetaPatternParser;
040import org.apache.wicket.util.string.Strings;
041
042
043/**
044 * THIS CLASS IS INTENDED FOR INTERNAL USE IN IMPLEMENTING LOCALE SENSITIVE COMPONENTS THAT USE
045 * IMAGE RESOURCES AND SHOULD NOT BE USED DIRECTLY BY END-USERS.
046 * <p>
047 * This class contains the logic for extracting static image resources referenced by the SRC
048 * attribute of component tags and keeping these static image resources in sync with the component
049 * locale.
050 * <p>
051 * If no image is specified by the SRC attribute of an IMG tag, then any VALUE attribute is
052 * inspected. If there is a VALUE attribute, it must be of the form
053 * "[factoryName]:[sharedImageName]?:[specification]". [factoryName] is the name of a resource
054 * factory that has been added to Application (for example, DefaultButtonImageResourceFactory is
055 * installed by default under the name "buttonFactory"). The [sharedImageName] value is optional and
056 * gives a name under which a given generated image is shared. For example, a cancel button image
057 * generated by the VALUE attribute "buttonFactory:cancelButton:Cancel" is shared under the name
058 * "cancelButton" and this specification will cause a component to reference the same image resource
059 * no matter what page it appears on, which is a very convenient and efficient way to create and
060 * share images. The [specification] string which follows the second colon is passed directly to the
061 * image factory and its format is dependent on the specific image factory. For details on the
062 * default buttonFactory, see
063 * {@link org.apache.wicket.markup.html.image.resource.DefaultButtonImageResourceFactory}.
064 * <p>
065 * Finally, if there is no SRC attribute and no VALUE attribute, the Image component's model is
066 * inspected. If the model contains a resource or resource reference, this image is used, otherwise
067 * the model is converted to a String and that value is used as a path to load the image.
068 * 
069 * @author Jonathan Locke
070 */
071public final class LocalizedImageResource implements IClusterable
072{
073        private static final long serialVersionUID = 1L;
074
075        /**
076         * What kind of resource it is. TRUE==Resource is set, FALSE==ResourceReference is set, null
077         * none
078         */
079        private Boolean resourceKind;
080
081        /** The component that is referencing this image resource */
082        private final Component component;
083
084        /** The image resource this image component references */
085        private IResource resource;
086
087        /** The resource reference */
088        private ResourceReference resourceReference;
089
090        /** The resource parameters */
091        private PageParameters resourceParameters;
092
093        /** The locale of the image resource */
094        private Locale locale;
095
096        /** The style of the image resource */
097        private String style;
098
099        /** The component's variation (of the style) */
100        private String variation;
101
102        /**
103         * Parses image value specifications of the form "[factoryName]:
104         * [shared-image-name]?:[specification]"
105         * 
106         * @author Jonathan Locke
107         */
108        private static final class ImageValueParser extends MetaPatternParser
109        {
110                /** Factory name */
111                private static final Group factoryName = new Group(MetaPattern.VARIABLE_NAME);
112
113                /** Image reference name */
114                private static final Group imageReferenceName = new Group(MetaPattern.VARIABLE_NAME);
115
116                /** Factory specification string */
117                private static final Group specification = new Group(MetaPattern.ANYTHING_NON_EMPTY);
118
119                /** Meta pattern. */
120                private static final MetaPattern pattern = new MetaPattern(factoryName, MetaPattern.COLON,
121                        new OptionalMetaPattern(new MetaPattern[] { imageReferenceName }), MetaPattern.COLON,
122                        specification);
123
124                /**
125                 * Construct.
126                 * 
127                 * @param input
128                 *            to parse
129                 */
130                private ImageValueParser(final CharSequence input)
131                {
132                        super(pattern, input);
133                }
134
135                /**
136                 * @return The factory name
137                 */
138                private String getFactoryName()
139                {
140                        return factoryName.get(matcher());
141                }
142
143                /**
144                 * @return Returns the imageReferenceName.
145                 */
146                private String getImageReferenceName()
147                {
148                        return imageReferenceName.get(matcher());
149                }
150
151                /**
152                 * @return Returns the specification.
153                 */
154                private String getSpecification()
155                {
156                        return specification.get(matcher());
157                }
158        }
159
160        /**
161         * Constructor
162         * 
163         * @param component
164         *            The component that owns this localized image resource
165         */
166        public LocalizedImageResource(final Component component)
167        {
168                this.component = component;
169                locale = component.getLocale();
170                style = component.getStyle();
171                variation = component.getVariation();
172        }
173
174        /**
175         * Binds this resource if it is shared
176         */
177        public final void bind()
178        {
179                // If we have a resource reference
180                if (resourceReference != null && resourceReference.canBeRegistered() &&
181                        Application.exists())
182                {
183                        // Bind the reference to the application
184                        Application.get()
185                                .getResourceReferenceRegistry()
186                                .registerResourceReference(resourceReference);
187                }
188        }
189
190        /**
191         * @param parameters
192         *            page parameters
193         */
194        public final void onResourceRequested(PageParameters parameters)
195        {
196                bind();
197                RequestCycle requestCycle = RequestCycle.get();
198                Attributes attributes = new Attributes(requestCycle.getRequest(),
199                        requestCycle.getResponse(), parameters);
200                resource.respond(attributes);
201        }
202
203        /**
204         * @param resource
205         *            The resource to set.
206         */
207        public final void setResource(final IResource resource)
208        {
209                if (this.resource != resource)
210                {
211                        resourceKind = Boolean.TRUE;
212                        this.resource = resource;
213                }
214        }
215
216        /**
217         * @param resourceReference
218         *            The resource to set.
219         */
220        public final void setResourceReference(final ResourceReference resourceReference)
221        {
222                setResourceReference(resourceReference, resourceParameters);
223        }
224
225        /**
226         * @return true if it has a resourceReference. (it points to a shared resource)
227         */
228        public final boolean isStateless()
229        {
230                return resourceReference != null;
231        }
232
233        /**
234         * @param resourceReference
235         *            The resource to set.
236         * @param resourceParameters
237         *            The resource parameters for the shared resource
238         */
239        public final void setResourceReference(final ResourceReference resourceReference,
240                final PageParameters resourceParameters)
241        {
242                if (resourceReference != this.resourceReference)
243                {
244                        resourceKind = Boolean.FALSE;
245                        this.resourceReference = resourceReference;
246                }
247                this.resourceParameters = resourceParameters;
248                bind();
249        }
250
251        /**
252         * @param tag
253         *            The tag to inspect for an optional src attribute that might reference an image.
254         * @throws WicketRuntimeException
255         *             Thrown if an image is required by the caller, but none can be found.
256         */
257        public final void setSrcAttribute(final ComponentTag tag)
258        {
259                // If locale has changed from the initial locale used to attach image
260                // resource, then we need to reload the resource in the new locale
261                Locale l = component.getLocale();
262                String s = component.getStyle();
263                String v = component.getVariation();
264                if (resourceKind == null &&
265                        (!Objects.equal(locale, l) || !Objects.equal(style, s) || !Objects.equal(variation, v)))
266                {
267                        // Get new component locale and style
268                        locale = l;
269                        style = s;
270                        variation = v;
271
272                        // Invalidate current resource so it will be reloaded/recomputed
273                        resourceReference = null;
274                        resource = null;
275                }
276                else
277                {
278                        // TODO post 1.2: should we have support for locale changes when the
279                        // resource reference (or resource??) is set manually..
280                        // We should get a new resource reference for the current locale
281                        // then that points to the same resource but with another locale if
282                        // it exists. Something like
283                        // SharedResource.getResourceReferenceForLocale(resourceReference);
284                }
285
286                // check if the model contains a resource, if so, load the resource from
287                // the model.
288                Object modelObject = component.getDefaultModelObject();
289                if (modelObject instanceof ResourceReference)
290                {
291                        resourceReference = (ResourceReference)modelObject;
292                }
293                else if (modelObject instanceof IResource)
294                {
295                        resource = (IResource)modelObject;
296                }
297
298                // Need to load image resource for this component?
299                if (resource == null && resourceReference == null)
300                {
301                        // Get SRC attribute of tag
302                        final CharSequence src = tag.getAttribute("src");
303                        if (src != null)
304                        {
305                                // Try to load static image
306                                loadStaticImage(src.toString());
307                        }
308                        else
309                        {
310                                // Get VALUE attribute of tag
311                                final CharSequence value = tag.getAttribute("value");
312                                if (value != null)
313                                {
314                                        // Try to generate an image using an image factory
315                                        newImage(value);
316                                }
317                                else
318                                {
319                                        // Load static image using model object as the path
320                                        loadStaticImage(component.getDefaultModelObjectAsString());
321                                }
322                        }
323                }
324
325                // Get URL for resource
326                final CharSequence url;
327                if (resourceReference != null)
328                {
329                        // Create URL to resource
330                        url = RequestCycle.get().urlFor(resourceReference, resourceParameters);
331                }
332                else
333                {
334                        // Create URL to component
335                        url = component.urlForListener(resourceParameters);
336                }
337
338                // Set the SRC attribute to point to the component or shared resource
339                tag.put("src", url);
340        }
341
342        /**
343         * @param application
344         *            The application
345         * @param factoryName
346         *            The name of the image resource factory
347         * @return The resource factory
348         * @throws WicketRuntimeException
349         *             Thrown if factory cannot be found
350         */
351        private IResourceFactory getResourceFactory(final Application application,
352                final String factoryName)
353        {
354                final IResourceFactory factory = application.getResourceSettings().getResourceFactory(
355                        factoryName);
356
357                // Found factory?
358                if (factory == null)
359                {
360                        throw new WicketRuntimeException("Could not find image resource factory named " +
361                                factoryName);
362                }
363                return factory;
364        }
365
366
367        static class SimpleStaticResourceReference extends ResourceReference
368        {
369                final IResource resource;
370
371                public SimpleStaticResourceReference(Class<?> scope, String name, Locale locale,
372                        String style, String variation, IResource resource)
373                {
374                        super(scope, name, locale, style, variation);
375                        this.resource = resource;
376                }
377
378                private static final long serialVersionUID = 1L;
379
380                @Override
381                public IResource getResource()
382                {
383                        return resource;
384                }
385
386        }
387
388        /**
389         * Tries to load static image at the given path and throws an exception if the image cannot be
390         * located.
391         * 
392         * @param path
393         *            The path to the image
394         * @throws WicketRuntimeException
395         *             Thrown if the image cannot be located
396         */
397        private void loadStaticImage(final String path)
398        {
399                MarkupContainer parent = component.findParentWithAssociatedMarkup();
400                if (parent instanceof Border)
401                {
402                        parent = parent.getParent();
403                }
404                final Class<?> scope = parent.getClass();
405                resourceReference = new PackageResourceReference(scope, path, locale, style, variation);
406                bind();
407        }
408
409        /**
410         * Generates an image resource based on the attribute values on tag
411         * 
412         * @param value
413         *            The value to parse
414         */
415        private void newImage(final CharSequence value)
416        {
417                // Parse value
418                final ImageValueParser valueParser = new ImageValueParser(value);
419
420                // Does value match parser?
421                if (valueParser.matches())
422                {
423                        final String imageReferenceName = valueParser.getImageReferenceName();
424                        final String specification = Strings.replaceHtmlEscapeNumber(valueParser.getSpecification());
425                        final String factoryName = valueParser.getFactoryName();
426                        final Application application = component.getApplication();
427
428                        // Do we have a reference?
429                        if (!Strings.isEmpty(imageReferenceName))
430                        {
431                                // Is resource already available via the application?
432                                if (application.getResourceReferenceRegistry().getResourceReference(
433                                        Application.class, imageReferenceName, locale, style, variation, true, false) == null)
434                                {
435                                        // Resource not available yet, so create it with factory and
436                                        // share via Application
437                                        final IResource imageResource = getResourceFactory(application, factoryName).newResource(
438                                                specification, locale, style, variation);
439
440                                        ResourceReference ref = new SimpleStaticResourceReference(Application.class,
441                                                imageReferenceName, locale, style, variation, imageResource);
442
443                                        application.getResourceReferenceRegistry().registerResourceReference(ref);
444                                }
445
446                                // Create resource reference
447                                resourceReference = new PackageResourceReference(Application.class,
448                                        imageReferenceName, locale, style, variation);
449                        }
450                        else
451                        {
452                                resource = getResourceFactory(application, factoryName).newResource(specification,
453                                        locale, style, variation);
454                        }
455                }
456                else
457                {
458                        throw new WicketRuntimeException(
459                                "Could not generate image for value attribute '" +
460                                        value +
461                                        "'.  Was expecting a value attribute of the form \"[resourceFactoryName]:[resourceReferenceName]?:[factorySpecification]\".");
462                }
463        }
464
465        /**
466         * return the resource
467         * 
468         * @return resource or <code>null</code> if there is none
469         */
470        public final IResource getResource()
471        {
472                return resource;
473        }
474
475        /**
476         * return the resource
477         * 
478         * @return resource or <code>null</code> if there is none
479         */
480        public final ResourceReference getResourceReference()
481        {
482                return resourceReference;
483        }
484}