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