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}