/* * 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.publish; import java.sql.Timestamp; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import javax.enterprise.context.RequestScoped; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.isis.applib.Identifier; 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.annotation.PublishedAction; import org.apache.isis.applib.annotation.PublishedObject; import org.apache.isis.applib.annotation.PublishedObject.ChangeKind; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.clock.ClockService; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.iactn.Interaction; import org.apache.isis.applib.services.iactn.InteractionContext; import org.apache.isis.applib.services.metrics.MetricsService; import org.apache.isis.applib.services.publish.EventMetadata; import org.apache.isis.applib.services.publish.EventPayload; import org.apache.isis.applib.services.publish.EventType; import org.apache.isis.applib.services.publish.ObjectStringifier; import org.apache.isis.applib.services.publish.PublishedObjects; import org.apache.isis.applib.services.publish.PublisherService; import org.apache.isis.applib.services.publish.PublishingService; import org.apache.isis.applib.services.user.UserService; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.oid.Oid; import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; import org.apache.isis.core.metamodel.adapter.oid.RootOid; import org.apache.isis.core.metamodel.facetapi.IdentifiedHolder; import org.apache.isis.core.metamodel.facets.FacetedMethod; import org.apache.isis.core.metamodel.facets.FacetedMethodParameter; import org.apache.isis.core.metamodel.facets.actions.action.invocation.CommandUtil; import org.apache.isis.core.metamodel.facets.actions.publish.PublishedActionFacet; import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; import org.apache.isis.core.metamodel.facets.object.publishedobject.PublishedObjectFacet; import org.apache.isis.core.metamodel.services.ixn.InteractionDtoServiceInternal; import org.apache.isis.core.metamodel.services.publishing.PublishingServiceInternal; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.runtime.services.changes.ChangedObjectsServiceInternal; import org.apache.isis.core.runtime.system.persistence.PersistenceSession; import org.apache.isis.core.runtime.system.session.IsisSessionFactory; /** * Wrapper around {@link PublishingService}. Is a no-op if there is no injected service. */ @DomainService( nature = NatureOfService.DOMAIN, menuOrder = "" + Integer.MAX_VALUE ) @RequestScoped public class PublishingServiceInternalDefault implements PublishingServiceInternal { private final static OidMarshaller OID_MARSHALLER = OidMarshaller.INSTANCE; //region > static helper functions private Function<ObjectAdapter, ObjectAdapter> notDestroyedElseEmpty() { return new Function<ObjectAdapter, ObjectAdapter>() { public ObjectAdapter apply(ObjectAdapter adapter) { if (adapter == null) { return null; } if (!adapter.isDestroyed()) { return adapter; } // objectstores such as JDO prevent the underlying pojo from being touched once it has been deleted. // we therefore replace that pojo with an 'empty' one. Object replacementObject = getPersistenceSession() .instantiateAndInjectServices(adapter.getSpecification()); getPersistenceSession().remapRecreatedPojo(adapter, replacementObject); return adapter; } }; } //endregion //region > publishObjects @Override @Programmatic public void publishObjects() { if(suppress) { return; } // take a copy of enlisted adapters ... the JDO implementation of the PublishingService // creates further entities which would be enlisted; taking copy of the map avoids ConcurrentModificationException final Map<ObjectAdapter, ChangeKind> changeKindByEnlistedAdapter = Maps.newHashMap(); changeKindByEnlistedAdapter.putAll(changedObjectsServiceInternal.getChangeKindByEnlistedAdapter()); publishObjectsToPublishingService(changeKindByEnlistedAdapter); publishObjectsToPublisherServices(changeKindByEnlistedAdapter); } private void publishObjectsToPublishingService(final Map<ObjectAdapter, ChangeKind> changeKindByEnlistedAdapter) { if(publishingServiceIfAny == null) { return; } final String currentUser = userService.getUser().getName(); final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp(); final ObjectStringifier stringifier = objectStringifier(); for (final Map.Entry<ObjectAdapter, ChangeKind> adapterAndChange : changeKindByEnlistedAdapter.entrySet()) { final ObjectAdapter enlistedAdapter = adapterAndChange.getKey(); final ChangeKind changeKind = adapterAndChange.getValue(); publishObjectToPublishingService( enlistedAdapter, changeKind, currentUser, timestamp, stringifier); } } private void publishObjectToPublishingService( final ObjectAdapter enlistedAdapter, final ChangeKind changeKind, final String currentUser, final Timestamp timestamp, final ObjectStringifier stringifier) { final PublishedObjectFacet publishedObjectFacet = enlistedAdapter.getSpecification().getFacet(PublishedObjectFacet.class); if(publishedObjectFacet == null) { return; } final PublishedObject.PayloadFactory payloadFactory = publishedObjectFacet.value(); final RootOid enlistedAdapterOid = (RootOid) enlistedAdapter.getOid(); final String enlistedAdapterClass = CommandUtil.targetClassNameFor(enlistedAdapter); final Bookmark enlistedTarget = enlistedAdapterOid.asBookmark(); final EventMetadata metadata = newEventMetadata( currentUser, timestamp, changeKind, enlistedAdapterClass, enlistedTarget); final Object pojo = ObjectAdapter.Util.unwrap(undeletedElseEmpty(enlistedAdapter)); final EventPayload payload = payloadFactory.payloadFor(pojo, changeKind); payload.withStringifier(stringifier); publishingServiceIfAny.publish(metadata, payload); } private void publishObjectsToPublisherServices( final Map<ObjectAdapter, ChangeKind> changeKindByEnlistedAdapter) { final Map<ObjectAdapter, ChangeKind> changeKindByPublishedAdapter = Maps.filterKeys( changeKindByEnlistedAdapter, PublishedObjectFacet.Predicates.isPublished()); if(changeKindByPublishedAdapter.isEmpty()) { return; } final int numberLoaded = metricsService.numberObjectsLoaded(); final int numberObjectPropertiesModified = changedObjectsServiceInternal.numberObjectPropertiesModified(); final PublishedObjects publishedObjects = newPublishedObjects(numberLoaded, numberObjectPropertiesModified, changeKindByPublishedAdapter); for (PublisherService publisherService : publisherServices) { publisherService.publish(publishedObjects); } } private PublishedObjects newPublishedObjects( final int numberLoaded, final int numberObjectPropertiesModified, final Map<ObjectAdapter, ChangeKind> changeKindByPublishedAdapter) { final Command command = commandContext.getCommand(); final UUID transactionUuid = command.getTransactionId(); final String userName = userService.getUser().getName(); final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp(); final Interaction interaction = interactionContext.getInteraction(); final int nextEventSequence = interaction.next(Interaction.Sequence.INTERACTION.id()); return new PublishedObjectsDefault(transactionUuid, nextEventSequence, userName, timestamp, numberLoaded, numberObjectPropertiesModified, changeKindByPublishedAdapter); } //endregion //region > publishAction @Programmatic public void publishAction( final Interaction.Execution execution, final ObjectAction objectAction, final IdentifiedHolder identifiedHolder, final ObjectAdapter targetAdapter, final List<ObjectAdapter> parameterAdapters, final ObjectAdapter resultAdapter) { if(suppress) { return; } publishActionToPublishingService( objectAction, identifiedHolder, targetAdapter, parameterAdapters, resultAdapter ); publishToPublisherServices(execution); } private void publishActionToPublishingService( final ObjectAction objectAction, final IdentifiedHolder identifiedHolder, final ObjectAdapter targetAdapter, final List<ObjectAdapter> parameterAdapters, final ObjectAdapter resultAdapter) { if(publishingServiceIfAny == null) { return; } final String currentUser = userService.getUser().getName(); final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp(); final PublishedActionFacet publishedActionFacet = identifiedHolder.getFacet(PublishedActionFacet.class); if(publishedActionFacet == null) { return; } final RootOid adapterOid = (RootOid) targetAdapter.getOid(); final String oidStr = OID_MARSHALLER.marshal(adapterOid); final Identifier actionIdentifier = objectAction.getIdentifier(); final String title = oidStr + ": " + actionIdentifier.toNameParmsIdentityString(); final String actionTargetClass = CommandUtil.targetClassNameFor(targetAdapter); final String actionTargetAction = CommandUtil.targetMemberNameFor(objectAction); final Bookmark actionTarget = CommandUtil.bookmarkFor(targetAdapter); final String actionMemberIdentifier = CommandUtil.memberIdentifierFor(objectAction); final List<String> parameterNames; final List<Class<?>> parameterTypes; final Class<?> returnType; if(identifiedHolder instanceof FacetedMethod) { // should always be the case final FacetedMethod facetedMethod = (FacetedMethod) identifiedHolder; returnType = facetedMethod.getType(); final List<FacetedMethodParameter> parameters = facetedMethod.getParameters(); parameterNames = immutableList(Iterables.transform(parameters, FacetedMethodParameter.Functions.GET_NAME)); parameterTypes = immutableList(Iterables.transform(parameters, FacetedMethodParameter.Functions.GET_TYPE)); } else { parameterNames = null; parameterTypes = null; returnType = null; } final Interaction interaction = interactionContext.getInteraction(); final int nextEventSequence = interaction.next(Interaction.Sequence.PUBLISHED_EVENT.id()); final UUID transactionId = interaction.getTransactionId(); final EventMetadata metadata = new EventMetadata( transactionId, nextEventSequence, EventType.ACTION_INVOCATION, currentUser, timestamp, title, actionTargetClass, actionTargetAction, actionTarget, actionMemberIdentifier, parameterNames, parameterTypes, returnType); final PublishedAction.PayloadFactory payloadFactory = publishedActionFacet.value(); final ObjectStringifier stringifier = objectStringifier(); final EventPayload payload = payloadFactory.payloadFor( identifiedHolder.getIdentifier(), ObjectAdapter.Util.unwrap(undeletedElseEmpty(targetAdapter)), ObjectAdapter.Util.unwrap(undeletedElseEmpty(parameterAdapters)), ObjectAdapter.Util.unwrap(undeletedElseEmpty(resultAdapter))); payload.withStringifier(stringifier); publishingServiceIfAny.publish(metadata, payload); } private static <T> List<T> immutableList(final Iterable<T> iterable) { return Collections.unmodifiableList(Lists.newArrayList(iterable)); } private ObjectStringifier objectStringifier() { return new ObjectStringifier() { @Override public String toString(Object object) { if(object == null) { return null; } final ObjectAdapter adapter = isisSessionFactory.getCurrentSession() .getPersistenceSession().adapterFor(object); Oid oid = adapter.getOid(); return oid != null? oid.enString(): encodedValueOf(adapter); } private String encodedValueOf(ObjectAdapter adapter) { EncodableFacet facet = adapter.getSpecification().getFacet(EncodableFacet.class); return facet != null? facet.toEncodedString(adapter): adapter.toString(); } @Override public String classNameOf(Object object) { final ObjectAdapter adapter = getPersistenceSession().adapterFor(object); final String className = adapter.getSpecification().getFullIdentifier(); return className; } }; } private List<ObjectAdapter> undeletedElseEmpty(List<ObjectAdapter> parameters) { return Lists.newArrayList(Iterables.transform(parameters, notDestroyedElseEmpty())); } private ObjectAdapter undeletedElseEmpty(ObjectAdapter adapter) { return notDestroyedElseEmpty().apply(adapter); } private EventMetadata newEventMetadata( final String currentUser, final Timestamp timestamp, final ChangeKind changeKind, final String enlistedAdapterClass, final Bookmark enlistedTarget) { final EventType eventType = PublishingServiceInternalDefault.eventTypeFor(changeKind); final Interaction interaction = interactionContext.getInteraction(); final int nextEventSequence = interaction.next(Interaction.Sequence.PUBLISHED_EVENT.id()); final UUID transactionId = interaction.getTransactionId(); return new EventMetadata( transactionId, nextEventSequence, eventType, currentUser, timestamp, enlistedTarget.toString(), enlistedAdapterClass, null, enlistedTarget, null, null, null, null); } private static EventType eventTypeFor(ChangeKind changeKind) { if(changeKind == ChangeKind.UPDATE) { return EventType.OBJECT_UPDATED; } if(changeKind == ChangeKind.CREATE) { return EventType.OBJECT_CREATED; } if(changeKind == ChangeKind.DELETE) { return EventType.OBJECT_DELETED; } throw new IllegalArgumentException("unknown ChangeKind '" + changeKind + "'"); } //endregion //region > publishProperty @Override public void publishProperty( final Interaction.Execution execution) { if(suppress) { return; } publishToPublisherServices(execution); } //endregion //region > helper: publishToPublisherServices private void publishToPublisherServices(final Interaction.Execution<?,?> execution) { if(publisherServices == null || publisherServices.isEmpty()) { return; } for (final PublisherService publisherService : publisherServices) { publisherService.publish(execution); } } //endregion //region > suppress // this service is request scoped boolean suppress; @Programmatic @Override public <T> T withPublishingSuppressed(final Block<T> block) { try { suppress = true; return block.exec(); } finally { suppress = false; } } //endregion //region > injected services @javax.inject.Inject private List<PublisherService> publisherServices; @javax.inject.Inject private PublishingService publishingServiceIfAny; @javax.inject.Inject private ChangedObjectsServiceInternal changedObjectsServiceInternal; @javax.inject.Inject private InteractionDtoServiceInternal interactionDtoServiceInternal; @javax.inject.Inject private CommandContext commandContext; @javax.inject.Inject private InteractionContext interactionContext; @javax.inject.Inject private ClockService clockService; @javax.inject.Inject private UserService userService; @javax.inject.Inject private MetricsService metricsService; @javax.inject.Inject private IsisSessionFactory isisSessionFactory; private PersistenceSession getPersistenceSession() { return isisSessionFactory.getCurrentSession().getPersistenceSession(); } //endregion }