/* * 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.model.models; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; import org.apache.wicket.request.http.handler.RedirectRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.ContentDisposition; import org.apache.wicket.util.resource.AbstractResourceStream; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.apache.wicket.util.resource.StringResourceStream; import org.apache.isis.applib.Identifier; import org.apache.isis.applib.annotation.BookmarkPolicy; import org.apache.isis.applib.annotation.PromptStyle; import org.apache.isis.applib.annotation.Where; import org.apache.isis.applib.services.routing.RoutingService; import org.apache.isis.applib.value.Blob; import org.apache.isis.applib.value.Clob; import org.apache.isis.applib.value.NamedWithMimeType; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking; import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; import org.apache.isis.core.metamodel.adapter.oid.RootOid; import org.apache.isis.core.metamodel.consent.Consent; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.facetapi.Facet; import org.apache.isis.core.metamodel.facetapi.FacetHolder; import org.apache.isis.core.metamodel.facets.object.bookmarkpolicy.BookmarkPolicyFacet; import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; import org.apache.isis.core.metamodel.facets.object.promptStyle.PromptStyleFacet; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.spec.ActionType; import org.apache.isis.core.metamodel.spec.ObjectSpecId; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.viewer.wicket.model.common.PageParametersUtils; import org.apache.isis.viewer.wicket.model.mementos.ActionMemento; import org.apache.isis.viewer.wicket.model.mementos.ActionParameterMemento; import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento; import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames; /** * Models an action invocation, either the gathering of arguments for the * action's {@link Mode#PARAMETERS parameters}, or the handling of the * {@link Mode#RESULTS results} once invoked. */ public class ActionModel extends BookmarkableModel<ObjectAdapter> implements FormExecutorContext { private static final long serialVersionUID = 1L; private static final OidMarshaller OID_MARSHALLER = OidMarshaller.INSTANCE; private static final String NULL_ARG = "$nullArg$"; private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("([^=]+)=(.+)"); /** * Whether we are obtaining arguments (eg in a dialog), or displaying the * results */ private enum Mode { PARAMETERS, RESULTS } public ActionModel copy() { return new ActionModel(this); } ////////////////////////////////////////////////// // Factory methods ////////////////////////////////////////////////// /** * @param entityModel * @param action * @return */ public static ActionModel create(final EntityModel entityModel, final ObjectAction action) { final ActionMemento homePageActionMemento = ObjectAdapterMemento.Functions.fromAction().apply(action); final Mode mode = action.getParameterCount() > 0? Mode.PARAMETERS : Mode.RESULTS; return new ActionModel(entityModel, homePageActionMemento, mode); } public static ActionModel createForPersistent( final PageParameters pageParameters, final SpecificationLoader specificationLoader) { return new ActionModel(pageParameters, specificationLoader); } /** * Factory method for creating {@link PageParameters}. * * see {@link #ActionModel(PageParameters, SpecificationLoader)} */ public static PageParameters createPageParameters( final ObjectAdapter adapter, final ObjectAction objectAction, final ConcurrencyChecking concurrencyChecking) { final PageParameters pageParameters = PageParametersUtils.newPageParameters(); final String oidStr = concurrencyChecking == ConcurrencyChecking.CHECK? adapter.getOid().enString(): adapter.getOid().enStringNoVersion(); PageParameterNames.OBJECT_OID.addStringTo(pageParameters, oidStr); final ActionType actionType = objectAction.getType(); PageParameterNames.ACTION_TYPE.addEnumTo(pageParameters, actionType); final ObjectSpecification actionOnTypeSpec = objectAction.getOnType(); if (actionOnTypeSpec != null) { PageParameterNames.ACTION_OWNING_SPEC.addStringTo(pageParameters, actionOnTypeSpec.getFullIdentifier()); } final String actionId = determineActionId(objectAction); PageParameterNames.ACTION_ID.addStringTo(pageParameters, actionId); return pageParameters; } public static Entry<Integer, String> parse(final String paramContext) { final Matcher matcher = KEY_VALUE_PATTERN.matcher(paramContext); if (!matcher.matches()) { return null; } final int paramNum; try { paramNum = Integer.parseInt(matcher.group(1)); } catch (final Exception e) { // ignore return null; } final String oidStr; try { oidStr = matcher.group(2); } catch (final Exception e) { return null; } return new Map.Entry<Integer, String>() { @Override public Integer getKey() { return paramNum; } @Override public String getValue() { return oidStr; } @Override public String setValue(final String value) { return null; } }; } ////////////////////////////////////////////////// // BookmarkableModel ////////////////////////////////////////////////// public PageParameters getPageParameters() { final ObjectAdapter adapter = getTargetAdapter(); final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final PageParameters pageParameters = createPageParameters( adapter, objectAction, ConcurrencyChecking.NO_CHECK); // capture argument values final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray(); for(final ObjectAdapter argumentAdapter: argumentsAsArray) { final String encodedArg = encodeArg(argumentAdapter); PageParameterNames.ACTION_ARGS.addStringTo(pageParameters, encodedArg); } return pageParameters; } @Override public String getTitle() { final ObjectAdapter adapter = getTargetAdapter(); final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final StringBuilder buf = new StringBuilder(); final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray(); for(final ObjectAdapter argumentAdapter: argumentsAsArray) { if(buf.length() > 0) { buf.append(","); } buf.append(abbreviated(titleOf(argumentAdapter), 8)); } return adapter.titleString(null) + "." + objectAction.getName() + (buf.length()>0?"(" + buf.toString() + ")":""); } @Override public boolean hasAsRootPolicy() { return true; } ////////////////////////////////////////////////// // helpers ////////////////////////////////////////////////// private static String titleOf(final ObjectAdapter argumentAdapter) { return argumentAdapter!=null?argumentAdapter.titleString(null):""; } private static String abbreviated(final String str, final int maxLength) { return str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "..."; } private static String determineActionId(final ObjectAction objectAction) { final Identifier identifier = objectAction.getIdentifier(); if (identifier != null) { return identifier.toNameParmsIdentityString(); } // fallback (used for action sets) return objectAction.getId(); } public static Mode determineMode(final ObjectAction action) { return action.getParameterCount() > 0 ? Mode.PARAMETERS : Mode.RESULTS; } private final EntityModel entityModel; private final ActionMemento actionMemento; private Mode actionMode; /** * Lazily populated in {@link #getArgumentModel(ActionParameterMemento)} */ private final Map<Integer, ActionArgumentModel> arguments = Maps.newHashMap(); private ActionModel(final PageParameters pageParameters, final SpecificationLoader specificationLoader) { this(newEntityModelFrom(pageParameters), newActionMementoFrom(pageParameters, specificationLoader), actionModeFrom(pageParameters, specificationLoader)); setArgumentsIfPossible(pageParameters); setContextArgumentIfPossible(pageParameters); } private static ActionMemento newActionMementoFrom( final PageParameters pageParameters, final SpecificationLoader specificationLoader) { final ObjectSpecId owningSpec = ObjectSpecId.of(PageParameterNames.ACTION_OWNING_SPEC.getStringFrom(pageParameters)); final ActionType actionType = PageParameterNames.ACTION_TYPE.getEnumFrom(pageParameters, ActionType.class); final String actionNameParms = PageParameterNames.ACTION_ID.getStringFrom(pageParameters); return new ActionMemento(owningSpec, actionType, actionNameParms, specificationLoader); } private static Mode actionModeFrom( final PageParameters pageParameters, final SpecificationLoader specificationLoader) { final ActionMemento actionMemento = newActionMementoFrom(pageParameters, specificationLoader); if(actionMemento.getAction(specificationLoader).getParameterCount() == 0) { return Mode.RESULTS; } final List<String> listFrom = PageParameterNames.ACTION_ARGS.getListFrom(pageParameters); return !listFrom.isEmpty()? Mode.RESULTS: Mode.PARAMETERS; } private static EntityModel newEntityModelFrom(final PageParameters pageParameters) { final RootOid oid = oidFor(pageParameters); if(oid.isTransient()) { return null; } else { return new EntityModel(ObjectAdapterMemento.createPersistent(oid)); } } private static RootOid oidFor(final PageParameters pageParameters) { final String oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pageParameters); return OID_MARSHALLER.unmarshal(oidStr, RootOid.class); } private ActionModel(final EntityModel entityModel, final ActionMemento actionMemento, final Mode actionMode) { this.entityModel = entityModel; this.actionMemento = actionMemento; this.actionMode = actionMode; } @Override public EntityModel getParentEntityModel() { return entityModel; } /** * Copy constructor, as called by {@link #copy()}. */ private ActionModel(final ActionModel actionModel) { this.entityModel = actionModel.entityModel; this.actionMemento = actionModel.actionMemento; this.actionMode = actionModel.actionMode; //this.actionPrompt = actionModel.actionPrompt; primeArgumentModels(); final Map<Integer, ActionArgumentModel> argumentModelByIdx = actionModel.arguments; for (final Map.Entry<Integer,ActionArgumentModel> argumentModel : argumentModelByIdx.entrySet()) { setArgument(argumentModel.getKey(), argumentModel.getValue().getObject()); } this.formExecutor = actionModel.formExecutor; } private void setArgumentsIfPossible( final PageParameters pageParameters) { final List<String> args = PageParameterNames.ACTION_ARGS.getListFrom(pageParameters); final ObjectAction action = actionMemento.getAction(getSpecificationLoader()); final List<ObjectSpecification> parameterTypes = action.getParameterTypes(); for (int paramNum = 0; paramNum < args.size(); paramNum++) { final String encoded = args.get(paramNum); setArgument(paramNum, parameterTypes.get(paramNum), encoded); } } public boolean hasParameters() { return actionMode == ActionModel.Mode.PARAMETERS; } private boolean setContextArgumentIfPossible(final PageParameters pageParameters) { final String paramContext = PageParameterNames.ACTION_PARAM_CONTEXT.getStringFrom(pageParameters); if (paramContext == null) { return false; } final ObjectAction action = actionMemento.getAction(getSpecificationLoader()); final List<ObjectSpecification> parameterTypes = action.getParameterTypes(); final int parameterCount = parameterTypes.size(); final Map.Entry<Integer, String> mapEntry = parse(paramContext); final int paramNum = mapEntry.getKey(); if (paramNum >= parameterCount) { return false; } final String encoded = mapEntry.getValue(); setArgument(paramNum, parameterTypes.get(paramNum), encoded); return true; } private void setArgument(final int paramNum, final ObjectSpecification argSpec, final String encoded) { final ObjectAdapter argumentAdapter = decodeArg(argSpec, encoded); setArgument(paramNum, argumentAdapter); } private String encodeArg(final ObjectAdapter adapter) { if(adapter == null) { return NULL_ARG; } final ObjectSpecification objSpec = adapter.getSpecification(); if(objSpec.isEncodeable()) { final EncodableFacet encodeable = objSpec.getFacet(EncodableFacet.class); return encodeable.toEncodedString(adapter); } return adapter.getOid().enStringNoVersion(); } private ObjectAdapter decodeArg(final ObjectSpecification objSpec, final String encoded) { if(NULL_ARG.equals(encoded)) { return null; } if(objSpec.isEncodeable()) { final EncodableFacet encodeable = objSpec.getFacet(EncodableFacet.class); return encodeable.fromEncodedString(encoded); } try { final RootOid oid = RootOid.deStringEncoded(encoded); return getPersistenceSession().adapterFor(oid); } catch (final Exception e) { return null; } } private void setArgument(final int paramNum, final ObjectAdapter argumentAdapter) { final ObjectAction action = actionMemento.getAction(getSpecificationLoader()); final ObjectActionParameter actionParam = action.getParameters().get(paramNum); final ActionParameterMemento apm = new ActionParameterMemento(actionParam); final ActionArgumentModel actionArgumentModel = getArgumentModel(apm); actionArgumentModel.setObject(argumentAdapter); } public ActionArgumentModel getArgumentModel(final ActionParameterMemento apm) { final int i = apm.getNumber(); ActionArgumentModel actionArgumentModel = arguments.get(i); if (actionArgumentModel == null) { actionArgumentModel = new ScalarModel(entityModel, apm); final int number = actionArgumentModel.getParameterMemento().getNumber(); arguments.put(number, actionArgumentModel); } return actionArgumentModel; } public ObjectAdapter getTargetAdapter() { return entityModel.load(getConcurrencyChecking()); } protected ConcurrencyChecking getConcurrencyChecking() { return actionMemento.getConcurrencyChecking(); } public ActionMemento getActionMemento() { return actionMemento; } @Override protected ObjectAdapter load() { // from getObject()/reExecute detach(); // force re-execute // TODO: think we need another field to determine if args have been populated. final ObjectAdapter results = executeAction(); this.actionMode = Mode.RESULTS; return results; } // REVIEW: should provide this rendering context, rather than hardcoding. // the net effect currently is that class members annotated with // @Hidden(where=Where.ANYWHERE) or @Disabled(where=Where.ANYWHERE) will indeed // be hidden/disabled, but will be visible/enabled (perhaps incorrectly) // for any other value for Where public static final Where WHERE_FOR_ACTION_INVOCATION = Where.ANYWHERE; private ObjectAdapter executeAction() { final ObjectAdapter targetAdapter = getTargetAdapter(); final ObjectAdapter[] arguments = getArgumentsAsArray(); final ObjectAction action = getActionMemento().getAction(getSpecificationLoader()); // if this action is a mixin, then it will fill in the details automatically. final ObjectAdapter mixedInAdapter = null; final ObjectAdapter resultAdapter = action.executeWithRuleChecking( targetAdapter, mixedInAdapter, arguments, InteractionInitiatedBy.USER, WHERE_FOR_ACTION_INVOCATION); final List<RoutingService> routingServices = getServicesInjector().lookupServices(RoutingService.class); final Object result = resultAdapter != null ? resultAdapter.getObject() : null; for (RoutingService routingService : routingServices) { final boolean canRoute = routingService.canRoute(result); if(canRoute) { final Object routeTo = routingService.route(result); return routeTo != null? getPersistenceSession().adapterFor(routeTo): null; } } return resultAdapter; } public String getReasonDisabledIfAny() { final ObjectAdapter targetAdapter = getTargetAdapter(); final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final Consent usability = objectAction.isUsable( targetAdapter, InteractionInitiatedBy.USER, Where.OBJECT_FORMS); final String disabledReasonIfAny = usability.getReason(); return disabledReasonIfAny; } public boolean isVisible() { final ObjectAdapter targetAdapter = getTargetAdapter(); final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final Consent visibility = objectAction.isVisible( targetAdapter, InteractionInitiatedBy.USER, Where.OBJECT_FORMS); return visibility.isAllowed(); } public String getReasonInvalidIfAny() { final ObjectAdapter targetAdapter = getTargetAdapter(); final ObjectAdapter[] proposedArguments = getArgumentsAsArray(); final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final Consent validity = objectAction.isProposedArgumentSetValid(targetAdapter, proposedArguments, InteractionInitiatedBy.USER); return validity.isAllowed() ? null : validity.getReason(); } @Override public void setObject(final ObjectAdapter object) { throw new UnsupportedOperationException("target adapter for ActionModel cannot be changed"); } public ObjectAdapter[] getArgumentsAsArray() { if(this.arguments.size() < this.getActionMemento().getAction(getSpecificationLoader()).getParameterCount()) { primeArgumentModels(); } final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final ObjectAdapter[] arguments = new ObjectAdapter[objectAction.getParameterCount()]; for (int i = 0; i < arguments.length; i++) { final ActionArgumentModel actionArgumentModel = this.arguments.get(i); arguments[i] = actionArgumentModel.getObject(); } return arguments; } public void reset() { this.actionMode = determineMode(actionMemento.getAction(getSpecificationLoader())); } public void clearArguments() { for (final ActionArgumentModel actionArgumentModel : arguments.values()) { actionArgumentModel.reset(); } this.actionMode = determineMode(actionMemento.getAction(getSpecificationLoader())); } /** * Bookmarkable if the {@link ObjectAction action} has a {@link BookmarkPolicyFacet bookmark} policy * of {@link BookmarkPolicy#AS_ROOT root}, and has safe {@link ObjectAction#getSemantics() semantics}. */ public boolean isBookmarkable() { final ObjectAction action = getActionMemento().getAction(getSpecificationLoader()); final BookmarkPolicyFacet bookmarkPolicy = action.getFacet(BookmarkPolicyFacet.class); final boolean safeSemantics = action.getSemantics().isSafeInNature(); return bookmarkPolicy.value() == BookmarkPolicy.AS_ROOT && safeSemantics; } // ////////////////////////////////////// /** * Simply executes the action. * * Previously there was exception handling code here also, but this has now been centralized * within FormExecutorAbstract */ public ObjectAdapter execute() { final ObjectAdapter resultAdapter = this.getObject(); return resultAdapter; } // ////////////////////////////////////// public static IRequestHandler redirectHandler(final Object value) { if(value instanceof java.net.URL) { final java.net.URL url = (java.net.URL) value; return new RedirectRequestHandler(url.toString()); } return null; } public static IRequestHandler downloadHandler(final Object value) { if(value instanceof Clob) { final Clob clob = (Clob)value; return handlerFor(resourceStreamFor(clob), clob); } if(value instanceof Blob) { final Blob blob = (Blob)value; return handlerFor(resourceStreamFor(blob), blob); } return null; } private static IResourceStream resourceStreamFor(final Blob blob) { final IResourceStream resourceStream = new AbstractResourceStream() { private static final long serialVersionUID = 1L; @Override public InputStream getInputStream() throws ResourceStreamNotFoundException { return new ByteArrayInputStream(blob.getBytes()); } @Override public String getContentType() { return blob.getMimeType().toString(); } @Override public void close() throws IOException { } }; return resourceStream; } private static IResourceStream resourceStreamFor(final Clob clob) { final IResourceStream resourceStream = new StringResourceStream(clob.getChars(), clob.getMimeType().toString()); return resourceStream; } private static IRequestHandler handlerFor(final IResourceStream resourceStream, final NamedWithMimeType namedWithMimeType) { final ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(resourceStream, namedWithMimeType.getName()); handler.setContentDisposition(ContentDisposition.ATTACHMENT); return handler; } // ////////////////////////////////////// public List<ActionParameterMemento> primeArgumentModels() { final ObjectAction objectAction = getActionMemento().getAction(getSpecificationLoader()); final List<ObjectActionParameter> parameters = objectAction.getParameters(); final List<ActionParameterMemento> mementos = buildParameterMementos(parameters); for (final ActionParameterMemento apm : mementos) { getArgumentModel(apm); } return mementos; } private static List<ActionParameterMemento> buildParameterMementos(final List<ObjectActionParameter> parameters) { final List<ActionParameterMemento> parameterMementoList = Lists.transform(parameters, ObjectAdapterMemento.Functions.fromActionParameter()); // we copy into a new array list otherwise we get lazy evaluation = // reference to a non-serializable object return Lists.newArrayList(parameterMementoList); } ////////////////////////////////////////////////// private FormExecutor formExecutor; /** * A hint passed from one Wicket UI component to another. * * Mot actually used by the model itself. */ public FormExecutor getFormExecutor() { return formExecutor; } public void setFormExecutor(final FormExecutor formExecutor) { this.formExecutor = formExecutor; } ////////////////////////////////////////////////// @Override public PromptStyle getPromptStyle() { final PromptStyleFacet facet = getFacet(PromptStyleFacet.class); if(facet == null) { return null; } return facet.value() == PromptStyle.INLINE ? PromptStyle.INLINE : PromptStyle.DIALOG; } public <T extends Facet> T getFacet(final Class<T> facetType) { final FacetHolder facetHolder = getActionMemento().getAction(getSpecificationLoader()); return facetHolder.getFacet(facetType); } ////////////////////////////////////////////////// private InlinePromptContext inlinePromptContext; /** * Further hint, to support inline prompts... */ public InlinePromptContext getInlinePromptContext() { return inlinePromptContext; } public void setInlinePromptContext(InlinePromptContext inlinePromptContext) { this.inlinePromptContext = inlinePromptContext; } ////////////////////////////////////////////////// // Dependencies (from context) ////////////////////////////////////////////////// ServicesInjector getServicesInjector() { return getIsisSessionFactory().getServicesInjector(); } }