Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add f:selectItemGroups #4862

Merged
merged 14 commits into from Mar 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -984,6 +984,20 @@ public void populateApplicationConfiguration(Document toPopulate) {
}
faces_configElement.appendChild(componentElement);
}
{
Element componentElement = toPopulate.createElementNS(ns, "component");
{
Element component_typeElement = toPopulate.createElementNS(ns, "component-type");
component_typeElement.appendChild(toPopulate.createTextNode("jakarta.faces.SelectItemGroups"));
componentElement.appendChild(component_typeElement);
}
{
Element component_classElement = toPopulate.createElementNS(ns, "component-class");
component_classElement.appendChild(toPopulate.createTextNode("jakarta.faces.component.UISelectItemGroups"));
componentElement.appendChild(component_classElement);
}
faces_configElement.appendChild(componentElement);
}
{
Element componentElement = toPopulate.createElementNS(ns, "component");
{
Expand Down
Expand Up @@ -24,6 +24,7 @@
import jakarta.faces.component.UIParameter;
import jakarta.faces.component.UISelectItem;
import jakarta.faces.component.UISelectItems;
import jakarta.faces.component.UISelectItemGroups;
import jakarta.faces.component.UIViewAction;
import jakarta.faces.component.UIViewParameter;
import jakarta.faces.component.UIWebsocket;
Expand Down Expand Up @@ -100,6 +101,8 @@ public CoreLibrary(String namespace) {

this.addComponent("selectItems", UISelectItems.COMPONENT_TYPE, null);

this.addComponent("selectItemGroups", UISelectItemGroups.COMPONENT_TYPE, null);

addTagHandler("setPropertyActionListener", SetPropertyActionListenerHandler.class);

this.addComponent("subview", "jakarta.faces.NamingContainer", null);
Expand Down
99 changes: 99 additions & 0 deletions impl/src/main/java/com/sun/faces/util/ScopedRunner.java
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2021 Contributors to Eclipse Foundation.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package com.sun.faces.util;

import java.util.HashMap;
import java.util.Map;

import jakarta.faces.context.FacesContext;

/**
* This class helps in letting code run within its own scope. Such scope is defined by specific variables being
* available to EL within it. The request scope is used to store the variables.
*
* @author Arjan Tijms
*
*/
public class ScopedRunner {

private FacesContext context;
private Map<String, Object> scopedVariables;
private Map<String, Object> previousVariables = new HashMap<>();

public ScopedRunner(FacesContext context) {
this(context, new HashMap<>());
}

public ScopedRunner(FacesContext context, Map<String, Object> scopedVariables) {
this.context = context;
this.scopedVariables = scopedVariables;
}

/**
* Adds the given variable to this instance. Can be used in a builder-pattern.
*
* @param key the key name of the variable
* @param value the value of the variable
* @return this ScopedRunner, so adding variables and finally calling invoke can be chained.
*/
public ScopedRunner with(String key, Object value) {
scopedVariables.put(key, value);
return this;
}

/**
* Invokes the callback within the scope of the variables being given in the constructor.
* @param callback The callback.
*/
public void invoke(Runnable callback) {
try {
setNewScope();
callback.run();
} finally {
restorePreviousScope();
}
}

private void setNewScope() {
previousVariables.clear();

Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
for (Map.Entry<String, Object> entry : scopedVariables.entrySet()) {
Object previousVariable = requestMap.put(entry.getKey(), entry.getValue());
if (previousVariable != null) {
previousVariables.put(entry.getKey(), previousVariable);
}
}
}

private void restorePreviousScope() {
try {
Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
for (Map.Entry<String, Object> entry : scopedVariables.entrySet()) {
Object previousVariable = previousVariables.get(entry.getKey());
if (previousVariable != null) {
requestMap.put(entry.getKey(), previousVariable);
} else {
requestMap.remove(entry.getKey());
}
}
} finally {
previousVariables.clear();
}
}

}
26 changes: 26 additions & 0 deletions impl/src/main/java/com/sun/faces/util/Util.java
Expand Up @@ -41,6 +41,7 @@
import java.net.URLConnection;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
Expand All @@ -51,6 +52,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.naming.InitialContext;
import javax.naming.NamingException;
Expand Down Expand Up @@ -1569,4 +1572,27 @@ public static boolean isCdiAvailable(ServletContext servletContext) {
return result;
}

@SuppressWarnings("unchecked")
public static <T> Stream<T> stream(Object object) {
if (object == null) {
return Stream.empty();
} else if (object instanceof Stream) {
return (Stream<T>) object;
} else if (object instanceof Iterable) {
return (Stream<T>) StreamSupport.stream(((Iterable<?>) object).spliterator(), false);
} else if (object instanceof Map) {
return (Stream<T>) ((Map<?, ?>) object).entrySet().stream();
} else if (object instanceof int[]) {
return (Stream<T>) Arrays.stream((int[]) object).boxed();
} else if (object instanceof long[]) {
return (Stream<T>) Arrays.stream((long[]) object).boxed();
} else if (object instanceof double[]) {
return (Stream<T>) Arrays.stream((double[]) object).boxed();
} else if (object instanceof Object[]) {
return (Stream<T>) Arrays.stream((Object[]) object);
} else {
return (Stream<T>) Stream.of(object);
}
}

}
117 changes: 117 additions & 0 deletions impl/src/main/java/jakarta/faces/component/UISelectItemGroups.java
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2021 Contributors to Eclipse Foundation.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package jakarta.faces.component;

import static com.sun.faces.util.Util.coalesce;
import static com.sun.faces.util.Util.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

import com.sun.faces.util.ScopedRunner;

import jakarta.faces.model.SelectItem;
import jakarta.faces.model.SelectItemGroup;

/**
* <p class="changed_added_4_0">
* <strong>UISelectItemGroups</strong> is a component that may be nested inside a {@link UISelectMany} or {@link UISelectOne} component, and causes the addition
* of one or more {@link SelectItemGroup} of one or more {@link SelectItem} instances to the list of available options in the parent component. This component
* accepts only children of type {@link UISelectItems} or {@link UISelectItem}. The <code>value</code> attribute of this component, set either directly, or
* acquired indirectly via a {@link jakarta.el.ValueExpression}, can be an array or {@link Iterable} of items of any type which is acceptable by the
* <code>value</code> attribute of any nested {@link UISelectItems} or {@link UISelectItem} component.
* </p>
*
* @since 4.0
*/

public class UISelectItemGroups extends UISelectItems {

// -------------------------------------------------------------------------------------------------------------------------------------- Manifest Constants

/**
* <p>
* The standard component type for this component.
* </p>
*/
public static final String COMPONENT_TYPE = "jakarta.faces.SelectItemGroups";

// ---------------------------------------------------------------------------------------------------------------------------------- ValueHolder Properties

/**
* <p>
* Iterate over the <code>value</code> attribute and wrap each item in a new {@link SelectItemGroup} instance whereby the item is exposed as a request
* attribute under the key specified by the <code>var</code> property. This must allow any nested {@link UISelectItems} or {@link UISelectItem} component
* to access the item via their attributes. Finally return these {@link SelectItemGroup} instances as an ordered collection.
* </p>
*/
@Override
public Object getValue() {
List<SelectItemGroup> groups = new ArrayList<>();

createSelectItems(this, super.getValue(), SelectItemGroup::new, selectItemGroup -> {
List<SelectItem> items = new ArrayList<>();

for (UIComponent child : getChildren()) {
if (child instanceof UISelectItems) {
createSelectItems(child, ((UISelectItems) child).getValue(), SelectItem::new, items::add);
}
else if (child instanceof UISelectItem) {
items.add(createSelectItem(child, null, SelectItem::new));
}
}

selectItemGroup.setSelectItems(items);
groups.add(selectItemGroup);
});

return groups;
}

private <S extends SelectItem> void createSelectItems(UIComponent component, Object values, Supplier<S> supplier, Consumer<S> callback) {
Map<String, Object> attributes = component.getAttributes();
String var = coalesce((String) attributes.get("var"), "item");
stream(values).forEach(value -> new ScopedRunner(getFacesContext()).with(var, value).invoke(() -> callback.accept(createSelectItem(component, getItemValue(attributes, value), supplier))));
}

private static <S extends SelectItem> S createSelectItem(UIComponent component, Object value, Supplier<S> supplier) {
Map<String, Object> attributes = component.getAttributes();
Object itemValue = getItemValue(attributes, value);
Object itemLabel = attributes.get("itemLabel");
Object itemEscaped = coalesce(attributes.get("itemEscaped"), attributes.get("itemLabelEscaped")); // f:selectItem || f:selectItems -- TODO: this should be aligned in their APIs.
Object itemDisabled = attributes.get("itemDisabled");

S selectItem = supplier.get();
selectItem.setValue(itemValue);
selectItem.setLabel(String.valueOf(itemLabel != null ? itemLabel : selectItem.getValue()));
selectItem.setEscape(itemEscaped == null || Boolean.parseBoolean(itemEscaped.toString()));
selectItem.setDisabled(itemDisabled != null && Boolean.parseBoolean(itemDisabled.toString()));
return selectItem;
}

/**
* Returns item value attribute, taking into account any value expression which actually evaluates to null.
*/
private static Object getItemValue(Map<String, Object> attributes, Object defaultValue) {
Object itemValue = attributes.get("itemValue");
return itemValue != null || attributes.containsKey("itemValue") ? itemValue : defaultValue;
}

}
22 changes: 22 additions & 0 deletions impl/src/main/java/jakarta/faces/component/UISelectItems.java
Expand Up @@ -16,6 +16,7 @@

package jakarta.faces.component;

import jakarta.el.ValueExpression;
import jakarta.faces.model.SelectItem;

/**
Expand Down Expand Up @@ -75,6 +76,27 @@ public UISelectItems() {

}

// ---------------------------------------------------------------- Bindings

/**
* <p class="changed_added_4_0">
* Set the {@link ValueExpression} used to calculate the value for the specified attribute or property name, if any.
* In addition, if a {@link ValueExpression} is set for the <code>var</code> property, regardless of the value, throw an illegal argument exception.
* </p>
*
* @throws IllegalArgumentException If <code>name</code> is one of <code>id</code>, <code>parent</code>, or <code>var</code>.
* @throws NullPointerException {@inheritDoc}
*/
@Override
public void setValueExpression(String name, ValueExpression binding) {

if ("var".equals(name)) {
throw new IllegalArgumentException();
}

super.setValueExpression(name, binding);
}

// -------------------------------------------------------------- Properties

@Override
Expand Down