/* * 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.isis.viewer.wicket.ui.components.actionmenu.serviceactions; import java.util.List; import java.util.Map; import java.util.Set; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.wicket.MarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.AbstractLink; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.model.Model; import org.apache.isis.applib.annotation.NatureOfService; import org.apache.isis.applib.filter.Filters; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.facets.actions.notinservicemenu.NotInServiceMenuFacet; import org.apache.isis.core.metamodel.facets.all.named.NamedFacet; import org.apache.isis.core.metamodel.facets.members.order.MemberOrderFacet; import org.apache.isis.core.metamodel.facets.object.domainservice.DomainServiceFacet; import org.apache.isis.core.metamodel.spec.ActionType; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.Contributed; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.viewer.wicket.model.models.EntityModel; import org.apache.isis.viewer.wicket.model.models.ServiceActionsModel; import org.apache.isis.viewer.wicket.ui.components.actionmenu.CssClassFaBehavior; import org.apache.isis.viewer.wicket.ui.util.CssClassAppender; import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipBehavior; public final class ServiceActionUtil { private ServiceActionUtil(){} static void addLeafItem(final CssMenuItem menuItem, final ListItem<CssMenuItem> listItem, final MarkupContainer parent) { Fragment leafItem; if (!menuItem.isSeparator()) { leafItem = new Fragment("content", "leafItem", parent); AbstractLink subMenuItemLink = menuItem.getLink(); Label menuItemLabel = new Label("menuLinkLabel", menuItem.getName()); subMenuItemLink.addOrReplace(menuItemLabel); if (!menuItem.isEnabled()) { listItem.add(new CssClassAppender("disabled")); subMenuItemLink.setEnabled(false); TooltipBehavior tooltipBehavior = new TooltipBehavior(Model.of(menuItem.getDisabledReason())); listItem.add(tooltipBehavior); } if (menuItem.isPrototyping()) { subMenuItemLink.add(new CssClassAppender("prototype")); } leafItem.add(subMenuItemLink); String cssClassFa = menuItem.getCssClassFa(); if (Strings.isNullOrEmpty(cssClassFa)) { subMenuItemLink.add(new CssClassAppender("menuLinkSpacer")); } else { menuItemLabel.add(new CssClassFaBehavior(cssClassFa, menuItem.getCssClassFaPosition())); } String cssClass = menuItem.getCssClass(); if (!Strings.isNullOrEmpty(cssClass)) { subMenuItemLink.add(new CssClassAppender(cssClass)); } } else { leafItem = new Fragment("content", "empty", parent); listItem.add(new CssClassAppender("divider")); } listItem.add(leafItem); } enum SeparatorStrategy { WITH_SEPARATORS { List<CssMenuItem> applySeparatorStrategy(final CssMenuItem subMenuItem) { return withSeparators(subMenuItem); } }, WITHOUT_SEPARATORS { List<CssMenuItem> applySeparatorStrategy(final CssMenuItem subMenuItem) { final List<CssMenuItem> subMenuItems = subMenuItem.getSubMenuItems(); return subMenuItems; } }; abstract List<CssMenuItem> applySeparatorStrategy(final CssMenuItem subMenuItem); } static List<CssMenuItem> withSeparators(CssMenuItem subMenuItem) { final List<CssMenuItem> subMenuItems = subMenuItem.getSubMenuItems(); final List<CssMenuItem> cssMenuItemsWithSeparators = withSeparators(subMenuItems); subMenuItem.replaceSubMenuItems(cssMenuItemsWithSeparators); return cssMenuItemsWithSeparators; } static List<CssMenuItem> withSeparators(List<CssMenuItem> subMenuItems) { final List<CssMenuItem> itemsWithSeparators = Lists.newArrayList(); for (CssMenuItem menuItem : subMenuItems) { if(menuItem.requiresSeparator()) { if(!itemsWithSeparators.isEmpty()) { // bit nasty... we add a new separator item final CssMenuItem separatorItem = CssMenuItem.newMenuItem(menuItem.getName() + "-separator") .prototyping(menuItem.isPrototyping()) .build(); separatorItem.setSeparator(true); itemsWithSeparators.add(separatorItem); } menuItem.setRequiresSeparator(false); } itemsWithSeparators.add(menuItem); } return itemsWithSeparators; } static void addFolderItem(final CssMenuItem subMenuItem, final ListItem<CssMenuItem> listItem, final MarkupContainer parent, final SeparatorStrategy separatorStrategy) { listItem.add(new CssClassAppender("dropdown-submenu")); Fragment folderItem = new Fragment("content", "folderItem", parent); listItem.add(folderItem); folderItem.add(new Label("folderName", subMenuItem.getName())); final List<CssMenuItem> menuItems = separatorStrategy.applySeparatorStrategy(subMenuItem); ListView<CssMenuItem> subMenuItemsView = new ListView<CssMenuItem>("subMenuItems", menuItems) { @Override protected void populateItem(ListItem<CssMenuItem> listItem) { CssMenuItem subMenuItem = listItem.getModelObject(); if (subMenuItem.hasSubMenuItems()) { addFolderItem(subMenuItem, listItem, parent, SeparatorStrategy.WITHOUT_SEPARATORS); } else { addLeafItem(subMenuItem, listItem, parent); } } }; folderItem.add(subMenuItemsView); } public static List<CssMenuItem> buildMenu(final ServiceActionsModel appActionsModel) { final List<ObjectAdapter> serviceAdapters = appActionsModel.getObject(); final List<ServiceAndAction> serviceActions = Lists.newArrayList(); for (final ObjectAdapter serviceAdapter : serviceAdapters) { collateServiceActions(serviceAdapter, ActionType.USER, serviceActions); collateServiceActions(serviceAdapter, ActionType.PROTOTYPE, serviceActions); } final Set<String> serviceNamesInOrder = serviceNamesInOrder(serviceAdapters, serviceActions); final Map<String, List<ServiceAndAction>> serviceActionsByName = groupByServiceName(serviceActions); // prune any service names that have no service actions serviceNamesInOrder.retainAll(serviceActionsByName.keySet()); return buildMenuItems(serviceNamesInOrder, serviceActionsByName); } /** * Builds a hierarchy of {@link CssMenuItem}s, following the provided map of {@link ServiceAndAction}s (keyed by their service Name). */ private static List<CssMenuItem> buildMenuItems( final Set<String> serviceNamesInOrder, final Map<String, List<ServiceAndAction>> serviceActionsByName) { final List<CssMenuItem> menuItems = Lists.newArrayList(); for (String serviceName : serviceNamesInOrder) { final CssMenuItem serviceMenuItem = CssMenuItem.newMenuItem(serviceName).build(); final List<ServiceAndAction> serviceActionsForName = serviceActionsByName.get(serviceName); for (ServiceAndAction serviceAndAction : serviceActionsForName) { final CssMenuItem.Builder subMenuItemBuilder = serviceMenuItem.newSubMenuItem(serviceAndAction); if (subMenuItemBuilder == null) { // either service or this action is not visible continue; } subMenuItemBuilder.build(); } if (serviceMenuItem.hasSubMenuItems()) { menuItems.add(serviceMenuItem); } } return menuItems; } // ////////////////////////////////////// /** * Spin through all object actions of the service adapter, and add to the provided List of {@link ServiceAndAction}s. */ private static void collateServiceActions( final ObjectAdapter serviceAdapter, final ActionType actionType, final List<ServiceAndAction> serviceActions) { final ObjectSpecification serviceSpec = serviceAdapter.getSpecification(); // skip if annotated to not be included in repository menu using @DomainService final DomainServiceFacet domainServiceFacet = serviceSpec.getFacet(DomainServiceFacet.class); if (domainServiceFacet != null) { final NatureOfService natureOfService = domainServiceFacet.getNatureOfService(); if (natureOfService == NatureOfService.VIEW_REST_ONLY || natureOfService == NatureOfService.VIEW_CONTRIBUTIONS_ONLY || natureOfService == NatureOfService.DOMAIN) { return; } } for (final ObjectAction objectAction : serviceSpec.getObjectActions( actionType, Contributed.INCLUDED, Filters.<ObjectAction>any())) { // skip if annotated to not be included in repository menu using legacy mechanism if (objectAction.getFacet(NotInServiceMenuFacet.class) != null) { continue; } final MemberOrderFacet memberOrderFacet = objectAction.getFacet(MemberOrderFacet.class); String serviceName = memberOrderFacet != null? memberOrderFacet.name(): null; if(Strings.isNullOrEmpty(serviceName)){ serviceName = serviceSpec.getFacet(NamedFacet.class).value(); } final EntityModel serviceModel = new EntityModel(serviceAdapter); serviceActions.add(new ServiceAndAction(serviceName, serviceModel, objectAction)); } } /** * The unique service names, as they appear in order of the provided List of {@link ServiceAndAction}s. * @param serviceAdapters */ private static Set<String> serviceNamesInOrder( final List<ObjectAdapter> serviceAdapters, final List<ServiceAndAction> serviceActions) { final Set<String> serviceNameOrder = Sets.newLinkedHashSet(); // first, order as defined in isis.properties for (ObjectAdapter serviceAdapter : serviceAdapters) { final ObjectSpecification serviceSpec = serviceAdapter.getSpecification(); String serviceName = serviceSpec.getFacet(NamedFacet.class).value(); serviceNameOrder.add(serviceName); } // then, any other services (eg due to misspellings, at the end) for (ServiceAndAction serviceAction : serviceActions) { if(!serviceNameOrder.contains(serviceAction.serviceName)) { serviceNameOrder.add(serviceAction.serviceName); } } return serviceNameOrder; } /** * Group the provided {@link ServiceAndAction}s by their service name. */ private static Map<String, List<ServiceAndAction>> groupByServiceName(final List<ServiceAndAction> serviceActions) { final Map<String, List<ServiceAndAction>> serviceActionsByName = Maps.newTreeMap(); // map available services ObjectAdapter lastServiceAdapter = null; for (ServiceAndAction serviceAction : serviceActions) { List<ServiceAndAction> serviceActionsForName = serviceActionsByName.get(serviceAction.serviceName); final ObjectAdapter serviceAdapter = serviceAction.serviceEntityModel.load(AdapterManager.ConcurrencyChecking.NO_CHECK); if(serviceActionsForName == null) { serviceActionsForName = Lists.newArrayList(); serviceActionsByName.put(serviceAction.serviceName, serviceActionsForName); } else { // capture whether this action is from a different service; if so, add a separator before it serviceAction.separator = lastServiceAdapter != serviceAdapter; } serviceActionsForName.add(serviceAction); lastServiceAdapter = serviceAdapter; } return serviceActionsByName; } }