/* | |
* Licensed to the Apache Software Foundation (ASF) under one or more | |
* contributor license agreements. See the NOTICE file distributed with | |
* this work for additional information regarding copyright ownership. | |
* The ASF licenses this file to You under the Apache License, Version 2.0 | |
* (the "License"); you may not use this file except in compliance with | |
* the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package org.apache.wicket.markup.html.form; | |
import java.io.Serializable; | |
import org.apache.wicket.Component; | |
import org.apache.wicket.MarkupContainer; | |
import org.apache.wicket.MetaDataKey; | |
import org.apache.wicket.WicketRuntimeException; | |
import org.apache.wicket.ajax.AjaxRequestTarget; | |
import org.apache.wicket.core.request.handler.ComponentNotFoundException; | |
import org.apache.wicket.core.util.string.CssUtils; | |
import org.apache.wicket.markup.ComponentTag; | |
import org.apache.wicket.markup.MarkupStream; | |
import org.apache.wicket.markup.html.TransparentWebMarkupContainer; | |
import org.apache.wicket.markup.resolver.IComponentResolver; | |
import org.apache.wicket.util.visit.IVisit; | |
import org.apache.wicket.util.visit.IVisitor; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* Resolver that implements the {@code wicket:for} attribute functionality. The attribute makes it | |
* easy to set up {@code <label>} tags for form components by providing the following features | |
* without having to add any additional components in code: | |
* <ul> | |
* <li>Outputs the {@code for} attribute with the value equivalent to the markup id of the | |
* referenced form component</li> | |
* <li>Appends {@code required} css class to the {@code <label>} tag if the referenced form | |
* component is required. Name of the css class can be overwritten by having a i18n property defined | |
* for key AutoLabel.CSS.required</li> | |
* <li>Appends {@code error} css class to the {@code <label>} tag if the referenced form component | |
* has failed validation. Name of the css class can be overwritten by having a i18n property defined | |
* for key AutoLabel.CSS.error</li> | |
* <li>Appends {@code disabled} css class to the {@code <label>} tag if the referenced form | |
* component has is not enabled in hierarchy. Name of the css class can be overwritten by having a i18n property defined | |
* for key AutoLabel.CSS.disabled</li> | |
* </ul> | |
* | |
* <p> | |
* The value of the {@code wicket:for} attribute can either contain an id of the form component or a | |
* path to it using the standard {@code :} path separator. Note that {@code ..} can be used as part | |
* of the path to construct a reference to the parent container, eg {@code ..:..:foo:bar}. First the | |
* value of the attribute will be treated as a path and the {@code <label>} tag's closest parent | |
* container will be queried for the form component. If the form component cannot be resolved the | |
* value of the {@code wicket:for} attribute will be treated as an id and all containers will be | |
* searched from the closest parent to the page. | |
* </p> | |
* | |
* @author igor | |
* @author Carl-Eric Menzel <cmenzel@wicketbuch.de> | |
*/ | |
public class AutoLabelResolver implements IComponentResolver | |
{ | |
private static final long serialVersionUID = 1L; | |
private static final Logger logger = LoggerFactory.getLogger(AutoLabelResolver.class); | |
static final String WICKET_FOR = ":for"; | |
public static final String LABEL_ATTR = "label_attr"; | |
public static final String CSS_REQUIRED_KEY = CssUtils.key(AutoLabel.class, "required"); | |
public static final String CSS_DISABLED_KEY = CssUtils.key(AutoLabel.class, "disabled"); | |
public static final String CSS_ERROR_KEY = CssUtils.key(AutoLabel.class, "error"); | |
private static final String CSS_DISABLED_DEFAULT = "disabled"; | |
private static final String CSS_REQUIRED_DEFAULT = "required"; | |
private static final String CSS_ERROR_DEFAULT = "error"; | |
@Override | |
public Component resolve(final MarkupContainer container, final MarkupStream markupStream, | |
final ComponentTag tag) | |
{ | |
if (!tag.getId().startsWith(LABEL_ATTR)) | |
{ | |
return null; | |
} | |
// retrieve the relative path to the component | |
final String path = tag.getAttribute(getWicketNamespace(markupStream) + WICKET_FOR).trim(); | |
Component component = findRelatedComponent(container, path); | |
if (component == null) | |
{ | |
throw new ComponentNotFoundException("Could not find form component with path '" + path + | |
"' while trying to resolve wicket:for attribute"); | |
} | |
// check if component implements ILabelProviderLocator | |
if (component instanceof ILabelProviderLocator) | |
{ | |
component = ((ILabelProviderLocator) component).getAutoLabelComponent(); | |
} | |
if (!(component instanceof ILabelProvider)) | |
{ | |
throw new WicketRuntimeException("Component '" + (component == null ? "null" : component.getClass().getName()) | |
+ "', pointed to by wicket:for attribute '" + path + "', does not implement " + ILabelProvider.class.getName()); | |
} | |
if (!component.getOutputMarkupId()) | |
{ | |
component.setOutputMarkupId(true); | |
if (component.hasBeenRendered()) | |
{ | |
logger.warn( | |
"Component: {} is referenced via a wicket:for attribute but does not have its outputMarkupId property set to true", | |
component.toString(false)); | |
} | |
} | |
if (component instanceof FormComponent) | |
{ | |
component.setMetaData(MARKER_KEY, new AutoLabelMarker((FormComponent<?>)component)); | |
} | |
return new AutoLabel(tag.getId(), component); | |
} | |
private String getWicketNamespace(MarkupStream markupStream) | |
{ | |
return markupStream.getWicketNamespace(); | |
} | |
/** | |
* | |
* @param container The container | |
* @param path The relative path to the component | |
* @return Component | |
*/ | |
static Component findRelatedComponent(MarkupContainer container, final String path) | |
{ | |
// try the quick and easy route first | |
Component component = container.get(path); | |
if (component != null) | |
{ | |
return component; | |
} | |
// try the long way, search the hierarchy from the closest container up to the page | |
final Component[] searched = new Component[] { null }; | |
while (container != null) | |
{ | |
component = container.visitChildren(Component.class, | |
new IVisitor<Component, Component>() | |
{ | |
@Override | |
public void component(Component child, IVisit<Component> visit) | |
{ | |
if (child == searched[0]) | |
{ | |
// this container was already searched | |
visit.dontGoDeeper(); | |
return; | |
} | |
if (path.equals(child.getId())) | |
{ | |
visit.stop(child); | |
return; | |
} | |
} | |
}); | |
if (component != null) | |
{ | |
return component; | |
} | |
// remember the container so we dont search it again, and search the parent | |
searched[0] = container; | |
container = container.getParent(); | |
} | |
return null; | |
} | |
public static String getLabelIdFor(Component component) | |
{ | |
return component.getMarkupId() + "-w-lbl"; | |
} | |
public static final MetaDataKey<AutoLabelMarker> MARKER_KEY = new MetaDataKey<>() | |
{ | |
}; | |
/** | |
* Marker used to track whether or not a form component has an associated auto label by its mere | |
* presense as well as some attributes of the component across requests. | |
* | |
* @author igor | |
* | |
*/ | |
public static final class AutoLabelMarker implements Serializable | |
{ | |
public static final short VALID = 0x01; | |
public static final short REQUIRED = 0x02; | |
public static final short ENABLED = 0x04; | |
private short flags; | |
public AutoLabelMarker(FormComponent<?> component) | |
{ | |
setFlag(VALID, component.isValid()); | |
setFlag(REQUIRED, component.isRequired()); | |
setFlag(ENABLED, component.isEnabledInHierarchy()); | |
} | |
public void updateFrom(FormComponent<?> component, AjaxRequestTarget target) | |
{ | |
boolean valid = component.isValid(), required = component.isRequired(), enabled = component.isEnabledInHierarchy(); | |
if (isValid() != valid) | |
{ | |
target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);", | |
getLabelIdFor(component), component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT), | |
!valid)); | |
} | |
if (isRequired() != required) | |
{ | |
target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);", | |
getLabelIdFor(component), component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT), | |
required)); | |
} | |
if (isEnabled() != enabled) | |
{ | |
target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);", | |
getLabelIdFor(component), component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT), | |
!enabled)); | |
} | |
setFlag(VALID, valid); | |
setFlag(REQUIRED, required); | |
setFlag(ENABLED, enabled); | |
} | |
public boolean isValid() | |
{ | |
return getFlag(VALID); | |
} | |
public boolean isEnabled() | |
{ | |
return getFlag(ENABLED); | |
} | |
public boolean isRequired() | |
{ | |
return getFlag(REQUIRED); | |
} | |
private boolean getFlag(final int flag) | |
{ | |
return (flags & flag) != 0; | |
} | |
private void setFlag(final short flag, final boolean set) | |
{ | |
if (set) | |
{ | |
flags |= flag; | |
} | |
else | |
{ | |
flags &= ~flag; | |
} | |
} | |
} | |
/** | |
* Component that is attached to the {@code <label>} tag and takes care of writing out the label | |
* text as well as setting classes on the {@code <label>} tag | |
* | |
* @author igor | |
*/ | |
protected static class AutoLabel extends TransparentWebMarkupContainer | |
{ | |
private static final long serialVersionUID = 1L; | |
private final Component component; | |
public AutoLabel(String id, Component fc) | |
{ | |
super(id); | |
component = fc; | |
setMarkupId(getLabelIdFor(component)); | |
setOutputMarkupId(true); | |
} | |
@Override | |
protected void onComponentTag(ComponentTag tag) | |
{ | |
super.onComponentTag(tag); | |
tag.put("for", component.getMarkupId()); | |
if (component instanceof FormComponent) | |
{ | |
FormComponent<?> fc = (FormComponent<?>)component; | |
if (fc.isRequired()) | |
{ | |
tag.append("class", component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT), " "); | |
} | |
if (!fc.isValid()) | |
{ | |
tag.append("class", component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT), " "); | |
} | |
} | |
if (!component.isEnabledInHierarchy()) | |
{ | |
tag.append("class", component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT), " "); | |
} | |
} | |
/** | |
* @return the component this label points to, if any. | |
*/ | |
public Component getRelatedComponent() | |
{ | |
return component; | |
} | |
} | |
} |