blob: 4ca498a25f55745a2a3a45c586a08b7eaf449e40 [file] [log] [blame]
/*
* 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;
}
}
}