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.form;
018
019import java.io.Serializable;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.MarkupContainer;
023import org.apache.wicket.MetaDataKey;
024import org.apache.wicket.WicketRuntimeException;
025import org.apache.wicket.ajax.AjaxRequestTarget;
026import org.apache.wicket.core.request.handler.ComponentNotFoundException;
027import org.apache.wicket.core.util.string.CssUtils;
028import org.apache.wicket.markup.ComponentTag;
029import org.apache.wicket.markup.MarkupStream;
030import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
031import org.apache.wicket.markup.resolver.IComponentResolver;
032import org.apache.wicket.util.visit.IVisit;
033import org.apache.wicket.util.visit.IVisitor;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * Resolver that implements the {@code wicket:for} attribute functionality. The attribute makes it
039 * easy to set up {@code <label>} tags for form components by providing the following features
040 * without having to add any additional components in code:
041 * <ul>
042 * <li>Outputs the {@code for} attribute with the value equivalent to the markup id of the
043 * referenced form component</li>
044 * <li>Appends {@code required} css class to the {@code <label>} tag if the referenced form
045 * component is required. Name of the css class can be overwritten by having a i18n property defined
046 * for key AutoLabel.CSS.required</li>
047 * <li>Appends {@code error} css class to the {@code <label>} tag if the referenced form component
048 * has failed validation. Name of the css class can be overwritten by having a i18n property defined
049 * for key AutoLabel.CSS.error</li>
050 * <li>Appends {@code disabled} css class to the {@code <label>} tag if the referenced form
051 * component has is not enabled in hierarchy. Name of the css class can be overwritten by having a i18n property defined
052 * for key AutoLabel.CSS.disabled</li>
053 * </ul>
054 * 
055 * <p>
056 * The value of the {@code wicket:for} attribute can either contain an id of the form component or a
057 * path to it using the standard {@code :} path separator. Note that {@code ..} can be used as part
058 * of the path to construct a reference to the parent container, eg {@code ..:..:foo:bar}. First the
059 * value of the attribute will be treated as a path and the {@code <label>} tag's closest parent
060 * container will be queried for the form component. If the form component cannot be resolved the
061 * value of the {@code wicket:for} attribute will be treated as an id and all containers will be
062 * searched from the closest parent to the page.
063 * </p>
064 * 
065 * @author igor
066 * @author Carl-Eric Menzel
067 */
068public class AutoLabelResolver implements IComponentResolver
069{
070        private static final long serialVersionUID = 1L;
071
072        private static final Logger logger = LoggerFactory.getLogger(AutoLabelResolver.class);
073
074        static final String WICKET_FOR = ":for";
075        
076        public static final String LABEL_ATTR = "label_attr";
077        
078        public static final String CSS_REQUIRED_KEY = CssUtils.key(AutoLabel.class, "required");
079        public static final String CSS_DISABLED_KEY = CssUtils.key(AutoLabel.class, "disabled");
080        public static final String CSS_ERROR_KEY = CssUtils.key(AutoLabel.class, "error");
081        private static final String CSS_DISABLED_DEFAULT = "disabled";
082        private static final String CSS_REQUIRED_DEFAULT = "required";
083        private static final String CSS_ERROR_DEFAULT = "error";
084        
085
086
087        @Override
088        public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
089                final ComponentTag tag)
090        {
091                if (!tag.getId().startsWith(LABEL_ATTR))
092                {
093                        return null;
094                }
095
096                // retrieve the relative path to the component
097                final String path = tag.getAttribute(getWicketNamespace(markupStream) + WICKET_FOR).trim();
098
099                Component component = findRelatedComponent(container, path);
100                if (component == null)
101                {
102                        throw new ComponentNotFoundException("Could not find form component with path '" + path +
103                                "' while trying to resolve wicket:for attribute");
104                }
105                // check if component implements ILabelProviderLocator
106                if (component instanceof ILabelProviderLocator)
107                {
108                        component = ((ILabelProviderLocator) component).getAutoLabelComponent();
109                }
110
111                if (!(component instanceof ILabelProvider))
112                {
113                        throw new WicketRuntimeException("Component '" + (component == null ? "null" : component.getClass().getName())
114                                        + "', pointed to by wicket:for attribute '" + path + "', does not implement " + ILabelProvider.class.getName());
115                }
116
117                if (!component.getOutputMarkupId())
118                {
119                        component.setOutputMarkupId(true);
120                        if (component.hasBeenRendered())
121                        {
122                                logger.warn(
123                                        "Component: {} is referenced via a wicket:for attribute but does not have its outputMarkupId property set to true",
124                                        component.toString(false));
125                        }
126                }
127
128                if (component instanceof FormComponent)
129                {
130                        component.setMetaData(MARKER_KEY, new AutoLabelMarker((FormComponent<?>)component));
131                }
132
133                return new AutoLabel(tag.getId(), component);
134        }
135
136        private String getWicketNamespace(MarkupStream markupStream)
137        {
138                return markupStream.getWicketNamespace();
139        }
140
141        /**
142         * 
143         * @param container The container
144         * @param path The relative path to the component
145         * @return Component
146         */
147        static Component findRelatedComponent(MarkupContainer container, final String path)
148        {
149                // try the quick and easy route first
150
151                Component component = container.get(path);
152                if (component != null)
153                {
154                        return component;
155                }
156
157                // try the long way, search the hierarchy from the closest container up to the page
158
159                final Component[] searched = new Component[] { null };
160                while (container != null)
161                {
162                        component = container.visitChildren(Component.class,
163                                new IVisitor<Component, Component>()
164                                {
165                                        @Override
166                                        public void component(Component child, IVisit<Component> visit)
167                                        {
168                                                if (child == searched[0])
169                                                {
170                                                        // this container was already searched
171                                                        visit.dontGoDeeper();
172                                                        return;
173                                                }
174                                                if (path.equals(child.getId()))
175                                                {
176                                                        visit.stop(child);
177                                                        return;
178                                                }
179                                        }
180                                });
181
182                        if (component != null)
183                        {
184                                return component;
185                        }
186
187                        // remember the container so we dont search it again, and search the parent
188                        searched[0] = container;
189                        container = container.getParent();
190                }
191
192                return null;
193        }
194
195        public static String getLabelIdFor(Component component)
196        {
197                return component.getMarkupId() + "-w-lbl";
198        }
199
200        public static final MetaDataKey<AutoLabelMarker> MARKER_KEY = new MetaDataKey<>()
201        {
202        };
203
204        /**
205         * Marker used to track whether or not a form component has an associated auto label by its mere
206         * presense as well as some attributes of the component across requests.
207         * 
208         * @author igor
209         * 
210         */
211        public static final class AutoLabelMarker implements Serializable
212        {
213                public static final short VALID = 0x01;
214                public static final short REQUIRED = 0x02;
215                public static final short ENABLED = 0x04;
216
217                private short flags;
218
219                public AutoLabelMarker(FormComponent<?> component)
220                {
221                        setFlag(VALID, component.isValid());
222                        setFlag(REQUIRED, component.isRequired());
223                        setFlag(ENABLED, component.isEnabledInHierarchy());
224                }
225
226                public void updateFrom(FormComponent<?> component, AjaxRequestTarget target)
227                {
228                        boolean valid = component.isValid(), required = component.isRequired(), enabled = component.isEnabledInHierarchy();
229
230                        if (isValid() != valid)
231                        {
232                                target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
233                                        getLabelIdFor(component), component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT),
234                                        !valid));
235                        }
236
237                        if (isRequired() != required)
238                        {
239                                target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
240                                        getLabelIdFor(component), component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT),
241                                        required));
242                        }
243
244                        if (isEnabled() != enabled)
245                        {
246                                target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
247                                        getLabelIdFor(component), component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT),
248                                        !enabled));
249                        }
250
251                        setFlag(VALID, valid);
252                        setFlag(REQUIRED, required);
253                        setFlag(ENABLED, enabled);
254                }
255
256                public boolean isValid()
257                {
258                        return getFlag(VALID);
259                }
260
261                public boolean isEnabled()
262                {
263                        return getFlag(ENABLED);
264                }
265
266                public boolean isRequired()
267                {
268                        return getFlag(REQUIRED);
269                }
270
271                private boolean getFlag(final int flag)
272                {
273                        return (flags & flag) != 0;
274                }
275
276                private void setFlag(final short flag, final boolean set)
277                {
278                        if (set)
279                        {
280                                flags |= flag;
281                        }
282                        else
283                        {
284                                flags &= ~flag;
285                        }
286                }
287        }
288
289        /**
290         * Component that is attached to the {@code <label>} tag and takes care of writing out the label
291         * text as well as setting classes on the {@code <label>} tag
292         * 
293         * @author igor
294         */
295        protected static class AutoLabel extends TransparentWebMarkupContainer
296        {
297                private static final long serialVersionUID = 1L;
298
299                private final Component component;
300
301                public AutoLabel(String id, Component fc)
302                {
303                        super(id);
304                        component = fc;
305                        
306                        setMarkupId(getLabelIdFor(component));
307                        setOutputMarkupId(true);
308                }
309
310                @Override
311                protected void onComponentTag(ComponentTag tag)
312                {
313                        super.onComponentTag(tag);
314                        tag.put("for", component.getMarkupId());
315
316                        if (component instanceof FormComponent)
317                        {
318                                FormComponent<?> fc = (FormComponent<?>)component;
319                                if (fc.isRequired())
320                                {
321                                        tag.append("class", component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT), " ");
322                                }
323                                if (!fc.isValid())
324                                {
325                                        tag.append("class", component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT), " ");
326                                }
327                        }
328
329                        if (!component.isEnabledInHierarchy())
330                        {
331                                tag.append("class", component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT), " ");
332                        }
333                }
334
335
336                /**
337                 * @return the component this label points to, if any.
338                 */
339                public Component getRelatedComponent()
340                {
341                        return component;
342                }
343        }
344}