/** * 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.core.runtime.services.background; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.apache.isis.applib.annotation.DomainService; import org.apache.isis.applib.annotation.NatureOfService; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.services.background.ActionInvocationMemento; import org.apache.isis.applib.services.background.BackgroundCommandService; import org.apache.isis.applib.services.background.BackgroundCommandService2; import org.apache.isis.applib.services.background.BackgroundService2; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.factory.FactoryService; import org.apache.isis.core.commons.exceptions.IsisException; import org.apache.isis.core.commons.lang.ArrayExtensions; 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.action.invocation.CommandUtil; import org.apache.isis.core.metamodel.services.command.CommandDtoServiceInternal; 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.core.metamodel.spec.feature.ObjectMember; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.core.metamodel.specloader.classsubstitutor.JavassistEnhanced; import org.apache.isis.core.metamodel.specloader.specimpl.ObjectActionMixedIn; import org.apache.isis.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault; import org.apache.isis.core.runtime.system.session.IsisSessionFactory; import org.apache.isis.schema.cmd.v1.CommandDto; import javassist.util.proxy.MethodFilter; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; import javassist.util.proxy.ProxyObject; /** * Depends on an implementation of {@link org.apache.isis.applib.services.background.BackgroundCommandService} to * be configured. */ @DomainService( nature = NatureOfService.DOMAIN, menuOrder = "" + Integer.MAX_VALUE ) public class BackgroundServiceDefault implements BackgroundService2 { @Programmatic @PostConstruct public void init(Map<String,String> props) { } @Programmatic @PreDestroy public void shutdown() { } // ////////////////////////////////////// private ObjectSpecificationDefault getJavaSpecificationOfOwningClass(final Method method) { return getJavaSpecification(method.getDeclaringClass()); } private ObjectSpecificationDefault getJavaSpecification(final Class<?> cls) { final ObjectSpecification objectSpec = getSpecification(cls); if (!(objectSpec instanceof ObjectSpecificationDefault)) { throw new UnsupportedOperationException( "Only Java is supported " + "(specification is '" + objectSpec.getClass().getCanonicalName() + "')"); } return (ObjectSpecificationDefault) objectSpec; } private ObjectSpecification getSpecification(final Class<?> type) { return specificationLoader.loadSpecification(type); } // ////////////////////////////////////// @Programmatic @Override public <T> T execute(final T domainObject) { final Class<? extends Object> cls = domainObject.getClass(); final MethodHandler methodHandler = newMethodHandler(domainObject, null); return newProxy(cls, null, methodHandler); } @Override public <T> T executeMixin(Class<T> mixinClass, Object mixedIn) { final T mixin = factoryService.mixin(mixinClass, mixedIn); final MethodHandler methodHandler = newMethodHandler(mixin, mixedIn); return newProxy(mixinClass, mixedIn, methodHandler); } @SuppressWarnings("unchecked") private <T> T newProxy( final Class<? extends Object> cls, final Object mixedInIfAny, final MethodHandler methodHandler) { final ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setSuperclass(cls); proxyFactory.setInterfaces(ArrayExtensions.combine(cls.getInterfaces(), new Class<?>[] { JavassistEnhanced.class })); proxyFactory.setFilter(new MethodFilter() { @Override public boolean isHandled(final Method m) { // ignore finalize() return !m.getName().equals("finalize"); } }); final Class<T> proxySubclass = proxyFactory.createClass(); try { final T newInstance; if(mixedInIfAny == null) { newInstance = proxySubclass.newInstance(); } else { Constructor constructor = findConstructor(proxySubclass, mixedInIfAny); newInstance = (T) constructor.newInstance(mixedInIfAny); } final ProxyObject proxyObject = (ProxyObject) newInstance; proxyObject.setHandler(methodHandler); return newInstance; } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new IsisException(e); } } private <T> Constructor<?> findConstructor(final Class<T> proxySubclass, final Object mixedInIfAny) { final Constructor<?>[] constructors = proxySubclass.getConstructors(); for (Constructor<?> constructor : constructors) { final Class<?>[] parameterTypes = constructor.getParameterTypes(); if(parameterTypes.length == 1 && parameterTypes[0].isAssignableFrom(mixedInIfAny.getClass())) { return constructor; } } throw new IllegalArgumentException( String.format( "Could not locate 1-arg constructor for mixin type of '%s' accepting an instance of '%s'", proxySubclass, mixedInIfAny.getClass().getName())); } /** * * @param target - the object that is proxied, either a domain object or a mixin around a domain object * @param mixedInIfAny - if target is a mixin, then this is the domain object that is mixed-in to. */ private <T> MethodHandler newMethodHandler( final T target, final Object mixedInIfAny) { return new MethodHandler() { @Override public Object invoke( final Object proxied, final Method proxyMethod, final Method proxiedMethod, final Object[] args) throws Throwable { final boolean inheritedFromObject = proxyMethod.getDeclaringClass().equals(Object.class); if(inheritedFromObject) { return proxyMethod.invoke(target, args); } final ObjectSpecificationDefault targetObjSpec = getJavaSpecificationOfOwningClass(proxyMethod); final ObjectMember member = targetObjSpec.getMember(proxyMethod); if(member == null) { return proxyMethod.invoke(target, args); } if(!(member instanceof ObjectAction)) { throw new UnsupportedOperationException( "Only actions can be executed in the background " + "(method " + proxiedMethod.getName() + " represents a " + member.getFeatureType().name() + "')"); } ObjectAction action = (ObjectAction) member; final Object domainObject; if (mixedInIfAny == null) { domainObject = target; } else { domainObject = mixedInIfAny; // replace action with the mixedIn action of the domain object itself action = findMixedInAction(action, mixedInIfAny); } final ObjectAdapter domainObjectAdapter = getAdapterManager().adapterFor(domainObject); final String domainObjectClassName = CommandUtil.targetClassNameFor(domainObjectAdapter); final String targetActionName = CommandUtil.targetMemberNameFor(action); final ObjectAdapter[] argAdapters = adaptersFor(args); final String targetArgs = CommandUtil.argDescriptionFor(action, argAdapters); final Command command = commandContext.getCommand(); if(backgroundCommandService instanceof BackgroundCommandService2) { final BackgroundCommandService2 bcs2 = (BackgroundCommandService2) backgroundCommandService; final List<ObjectAdapter> targetList = Collections.singletonList(domainObjectAdapter); final CommandDto dto = commandDtoServiceInternal.asCommandDto(targetList, action, argAdapters); bcs2.schedule(dto, command, domainObjectClassName, targetActionName, targetArgs); } else { // fallback final ActionInvocationMemento aim = commandDtoServiceInternal.asActionInvocationMemento(proxyMethod, target, args); backgroundCommandService.schedule(aim, command, domainObjectClassName, targetActionName, targetArgs); } return null; } private ObjectAction findMixedInAction(final ObjectAction action, final Object domainObject) { final String actionId = action.getId(); final ObjectSpecification domainSpec = getAdapterManager().adapterFor(domainObject).getSpecification(); List<ObjectAction> objectActions = domainSpec.getObjectActions(Contributed.INCLUDED); for (ObjectAction objectAction : objectActions) { if(objectAction instanceof ObjectActionMixedIn) { ObjectActionMixedIn objectActionMixedIn = (ObjectActionMixedIn) objectAction; if(objectActionMixedIn.hasMixinAction(action)) { return objectActionMixedIn; } } } throw new IllegalArgumentException(String.format( "Unable to find mixin action '%s' for %s", actionId, domainSpec.getFullIdentifier())); } ObjectAdapter[] adaptersFor(final Object[] args) { final AdapterManager adapterManager = getAdapterManager(); return CommandUtil.adaptersFor(args, adapterManager); } }; } // ////////////////////////////////////// @Programmatic @Override public ActionInvocationMemento asActionInvocationMemento(Method method, Object domainObject, Object[] args) { throw new RuntimeException("Replaced by InteractionDtoServiceInternal"); } // ////////////////////////////////////// @javax.inject.Inject private BackgroundCommandService backgroundCommandService; @javax.inject.Inject private CommandDtoServiceInternal commandDtoServiceInternal; @javax.inject.Inject private CommandContext commandContext; @javax.inject.Inject private FactoryService factoryService; @javax.inject.Inject private SpecificationLoader specificationLoader; @javax.inject.Inject private IsisSessionFactory isisSessionFactory; protected AdapterManager getAdapterManager() { return isisSessionFactory.getCurrentSession().getPersistenceSession(); } }