blob: ac2933ccb40b0aeffaf0979ee7c870a5eeb08ecf [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.myfaces.tobago.internal.renderkit.renderer;
import org.apache.myfaces.tobago.component.ClientBehaviors;
import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
import org.apache.myfaces.tobago.internal.component.AbstractUICommand;
import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;
import org.apache.myfaces.tobago.internal.component.AbstractUIOperation;
import org.apache.myfaces.tobago.internal.renderkit.Collapse;
import org.apache.myfaces.tobago.internal.renderkit.Command;
import org.apache.myfaces.tobago.internal.renderkit.CommandMap;
import org.apache.myfaces.tobago.internal.util.RenderUtils;
import org.apache.myfaces.tobago.internal.util.StringUtils;
import org.apache.myfaces.tobago.util.ComponentUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.faces.component.ActionSource;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.AjaxBehavior;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorContext;
import javax.faces.context.FacesContext;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.PhaseId;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class TobagoClientBehaviorRenderer extends javax.faces.render.ClientBehaviorRenderer {
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* In standard JSF this method returns a JavaScript string. Because of CSP, Tobago doesn't render JavaScript
* into the HTML content. It transports the return value a bit hacky by {@link CommandMap#storeCommandMap}.
*
* @return "dummy" string or null, if nothing to do.
*/
@Override
public String getScript(final ClientBehaviorContext behaviorContext, final ClientBehavior behavior) {
final FacesContext facesContext = behaviorContext.getFacesContext();
final UIComponent uiComponent = behaviorContext.getComponent();
final ClientBehaviors eventName = ClientBehaviors.valueOf(behaviorContext.getEventName());
//// TBD: is this nice? May be implemented with a JSF behavior?
Collapse collapse = createCollapsible(facesContext, uiComponent);
String executeIds = null;
String renderIds = null;
Boolean transition = null;
String target = null;
String actionId = null;
boolean omit = false;
final String confirmation = ComponentUtils.getConfirmation(uiComponent);
if (behavior instanceof AjaxBehavior) {
final AjaxBehavior ajaxBehavior = (AjaxBehavior) behavior;
if (ajaxBehavior.isDisabled()) {
return null;
}
final Collection<String> execute = ajaxBehavior.getExecute();
final Collection<String> render = ajaxBehavior.getRender();
final String clientId = uiComponent.getClientId(facesContext);
executeIds = ComponentUtils.evaluateClientIds(facesContext, uiComponent, execute.toArray(new String[0]));
if (executeIds != null) {
executeIds = executeIds + " " + clientId;
} else {
executeIds = clientId;
}
if (uiComponent instanceof AbstractUICommand) { // <f:ajax> inside of a command
final AbstractUICommand command = (AbstractUICommand) uiComponent;
transition = command.isTransition();
target = command.getTarget();
omit = command.isOmit() || StringUtils.isNotBlank(RenderUtils.generateUrl(facesContext, command));
}
renderIds = ComponentUtils.evaluateClientIds(facesContext, uiComponent, render.toArray(new String[0]));
actionId = clientId;
} else if (behavior instanceof EventBehavior) { // <tc:event>
final EventBehavior eventBehavior = (EventBehavior) behavior;
final AbstractUIEvent event = RenderUtils.getAbstractUIEvent(uiComponent, eventBehavior);
if (event != null) {
if (!event.isRendered() || event.isDisabled()) {
return null;
} else {
transition = event.isTransition();
target = event.getTarget();
actionId = event.getClientId(facesContext);
if (collapse == null) {
collapse = createCollapsible(facesContext, event);
}
omit = event.isOmit() || StringUtils.isNotBlank(RenderUtils.generateUrl(facesContext, event));
}
}
} else {
LOG.warn("Unknown behavior '{}'!", behavior.getClass().getName());
}
final Command command = new Command(
actionId,
transition,
target,
executeIds,
renderIds,
null,
confirmation,
null,
collapse,
omit);
final CommandMap map = new CommandMap();
map.addCommand(eventName, command);
CommandMap.storeCommandMap(facesContext, map);
// XXX the return value is a string, but we should use a CommandMap
return "dummy";
}
@Override
public void decode(final FacesContext facesContext, final UIComponent component,
final ClientBehavior clientBehavior) {
if (clientBehavior instanceof AjaxBehavior) {
AjaxBehavior ajaxBehavior = (AjaxBehavior) clientBehavior;
if (!component.isRendered() || ajaxBehavior.isDisabled()) {
return;
}
dispatchBehaviorEvent(component, ajaxBehavior);
} else if (clientBehavior instanceof EventBehavior) {
final EventBehavior eventBehavior = (EventBehavior) clientBehavior;
final AbstractUIEvent abstractUIEvent = RenderUtils.getAbstractUIEvent(component, eventBehavior);
if (!component.isRendered() || abstractUIEvent == null
|| !abstractUIEvent.isRendered() || abstractUIEvent.isDisabled()) {
return;
}
for (List<ClientBehavior> children : abstractUIEvent.getClientBehaviors().values()) {
for (ClientBehavior child : children) {
decode(facesContext, component, child);
}
}
dispatchBehaviorEvent(component, eventBehavior);
}
}
private void dispatchBehaviorEvent(final UIComponent component, final ClientBehavior clientBehavior) {
final AjaxBehaviorEvent event = new AjaxBehaviorEvent(component, clientBehavior);
final boolean isImmediate = isImmediate(clientBehavior, component);
event.setPhaseId(isImmediate ? PhaseId.APPLY_REQUEST_VALUES : PhaseId.INVOKE_APPLICATION);
component.queueEvent(event);
}
private boolean isImmediate(final ClientBehavior clientBehavior, final UIComponent component) {
if (clientBehavior instanceof AjaxBehavior) {
AjaxBehavior ajaxBehavior = (AjaxBehavior) clientBehavior;
if (ajaxBehavior.isImmediateSet()) {
return ajaxBehavior.isImmediate();
}
} else if (clientBehavior instanceof EventBehavior) {
EventBehavior eventBehavior = (EventBehavior) clientBehavior;
if (eventBehavior.isImmediateSet()) {
return eventBehavior.isImmediate();
}
}
if (component instanceof EditableValueHolder) {
return ((EditableValueHolder) component).isImmediate();
}
if (component instanceof ActionSource) {
return ((ActionSource) component).isImmediate();
}
return false;
}
/**
* @deprecated TBD
*/
@Deprecated
public static Collapse createCollapsible(final FacesContext facesContext, final UIComponent component) {
//// TBD: is this nice? May be implemented with a JSF behavior?
//// BEGIN
// XXX too complicated
final List<AbstractUIOperation> operations = new ArrayList<>();
for (final UIComponent child : component.getChildren()) {
if (AbstractUIOperation.class.isAssignableFrom(child.getClass())) {
operations.add((AbstractUIOperation) child);
}
}
Collapse collapse = null;
if (operations.size() > 0) {
final AbstractUIOperation operation = operations.get(0);
final String forId = ComponentUtils.evaluateClientId(facesContext, component, operation.getFor());
collapse = new Collapse(Collapse.Action.valueOf(operation.getName()), forId);
}
//// TBD: is this nice?
//// END
return collapse;
}
}