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}