package org.easysoa.registry; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.easysoa.registry.systems.IntelligentSystemTreeService; import org.easysoa.registry.types.SoaNode; import org.easysoa.registry.types.ids.SoaNodeId; import org.easysoa.registry.types.listeners.EventListenerBase; import org.easysoa.registry.utils.RepositoryHelper; import org.nuxeo.common.collections.ScopeType; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.event.DocumentEventTypes; import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; import org.nuxeo.ecm.core.api.model.PropertyException; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventContext; import org.nuxeo.ecm.core.event.EventListener; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.runtime.api.Framework; /** * * @author mkalam-alami * */ public class RepositoryManagementListener extends EventListenerBase implements EventListener { private static final String CONTEXT_REQUEST_REPOSITORY_MANAGEMENT_LOOP_COUNT = "repositoryManagementLoopCount"; private static Logger logger = Logger.getLogger(RepositoryManagementListener.class); @Override public void handleEvent(Event event) throws ClientException { // Ensure event nature EventContext context = event.getContext(); if (!(context instanceof DocumentEventContext)) { return; } DocumentEventContext documentContext = (DocumentEventContext) context; DocumentModel sourceDocument = documentContext.getSourceDocument(); // Prevent looping on source document changes // (required at least when changing SOA name) if (areListenersDisabled(context)) { return; } int repositoryManagementLoopCount; if (context.hasProperty(CONTEXT_REQUEST_REPOSITORY_MANAGEMENT_LOOP_COUNT)) { repositoryManagementLoopCount = 1 + (Integer) context.getProperty(CONTEXT_REQUEST_REPOSITORY_MANAGEMENT_LOOP_COUNT); if (repositoryManagementLoopCount > 10) { throw new ClientException("RepositoryManagementListener : Infinite loop on " + sourceDocument); } } else { repositoryManagementLoopCount = 1; } sourceDocument.getContextData().putScopedValue(ScopeType.REQUEST, CONTEXT_REQUEST_REPOSITORY_MANAGEMENT_LOOP_COUNT, repositoryManagementLoopCount); if (!sourceDocument.hasSchema(SoaNode.SCHEMA)) { return; // nothing to do on non SOA nodes } if (sourceDocument.isVersion()) { logger.warn("RepositoryManagementListener : skipping because isVersion (but not checked out nor isBeingVersionedSubprojectNodeEvent())" //+ ", maybe isBeingVersionedSubprojectNodeEvent() " + SubprojectServiceImpl.isBeingVersionedSubprojectNodeEvent(event, sourceDocument) + ", event " + event.getName()); return; // nothing can be done on it since it is a version, internal proxies // are handled by tree snapshot itself (TODO nuxeo ID properties), and // outside references to it can only change through explicit action (updateToVersion) } if (SubprojectServiceImpl.isBeingVersionedSubprojectNodeEvent(event, sourceDocument)) { logger.warn("RepositoryManagementListener : skipping because isBeingVersionedSubprojectNodeEvent " + sourceDocument + ", event " + event.getName()); return; // this document is currently being tree snapshotted, do nothing here } if (sourceDocument.isProxy()) { // getting target document of proxy : DocumentModel proxyTargetDocument = documentContext.getCoreSession().getSourceDocument(sourceDocument.getRef()); // NB. same as : proxyTargetDocument = coreSession.getDocument(new org.nuxeo.ecm.core.api.IdRef(sourceDocument.getSourceId())); // NB. DON'T USE coreSession.getWorkingCopy(sourceDocument.getRef()) because it never returns a version ! if (proxyTargetDocument.isVersion()) { logger.warn("RepositoryManagementListener : skipping because is live proxy of version (but not isCheckedOut() nor isBeingVersionedSubprojectNodeEvent()) " + sourceDocument //+ ", maybe isCheckedOut " + sourceDocument.isCheckedOut() + " " //+ " or isBeingVersionedSubprojectNodeEvent() " + SubprojectServiceImpl.isBeingVersionedSubprojectNodeEvent(event, sourceDocument) + ", event " + event.getName()); return; // nothing can be done on it since it is a version, internal proxies // are handled by tree snapshot itself (TODO nuxeo ID properties), and // outside references to it can only change through explicit action (updateToVersion) } } // Initialize CoreSession documentManager = documentContext.getCoreSession(); DocumentService documentService; SoaMetamodelService metamodelService; try { documentService = Framework.getService(DocumentService.class); metamodelService = Framework.getService(SoaMetamodelService.class); } catch (Exception e) { logger.error("A required service is missing, aborting SoaNode repository management: " + e.getMessage()); return; } boolean documentModified = false; boolean isCreationOnCheckinMode = "ProjetWebServiceImpl".equals(sourceDocument.getPropertyValue("dc:title")); // TODO isJwt(OrCmis) using probeType boolean isCreationOnCheckin = isCreationOnCheckinMode && DocumentEventTypes.DOCUMENT_CHECKEDIN.equals(event.getName()); boolean isCreation = isCreationOnCheckin || !isCreationOnCheckinMode && DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName()); if (isCreationOnCheckinMode && DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName())) { sourceDocument.setPropertyValue("dc:description", "jwtJustCreated"); documentModified = true; } else if (!DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName()) // not a remove && (!isCreationOnCheckinMode || isCreationOnCheckin && "jwtJustCreated".equals(sourceDocument.getPropertyValue("dc:description")))) { // not a cmis creation (event handling reported at cmis checkin that will come next) if (isCreationOnCheckinMode && "jwtJustCreated".equals(sourceDocument.getPropertyValue("dc:description"))) { sourceDocument.setPropertyValue("dc:description", "done"); // cleaning documentModified = true; } try { // Validate or set soaname String newSoaName = metamodelService.validateIntegrity(sourceDocument, DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName())); // TODO not required if DocumentEventTypes.DOCUMENT_CREATED_BY_COPY : its soaname is already OK // NB. true copy is unavailable (triggers proxying), save in different Phase / subproject // because subproject meta is updated by its listener BEFORE this code (RepositoryManagementListener) happens // TODO still change soaname in this case // TODO LATER an alternative would be to have true "Create link / proxy" button in UI. if (newSoaName != null) { sourceDocument.setPropertyValue(SoaNode.XPATH_SOANAME, newSoaName); // TODO [soaname change] // NB. looping must be prevented here sourceDocument = documentManager.saveDocument(sourceDocument);// required when created using Nuxeo UI not in repo, else when moved is revalidated and set to null documentModified = false;//true } // Working copy/proxies management sourceDocument = manageRepositoryStructure(documentManager, documentService, sourceDocument, repositoryManagementLoopCount); documentManager.save(); // else loops on classifySoaNode() within IST's handleDocumentModel() below // Intelligent system trees update ///sourceDocument.getContextData().remove(ScopeType.REQUEST.getScopedKey( /// CONTEXT_REQUEST_REPOSITORY_ALREADY_MANAGED)); // allowing one RML loop, else InheritedDataTest.testUuidSelectors() fails IntelligentSystemTreeService intelligentSystemTreeServiceCache = Framework.getService(IntelligentSystemTreeService.class); intelligentSystemTreeServiceCache.handleDocumentModel(documentManager, sourceDocument, !isCreation); // creates System tree or proxy or moves sourceDocument, but doesn't in itself change the sourceDocument (?) } catch (ModelIntegrityException e) { String message = "Rollback needed because of integrity issue on " + sourceDocument.getTitle(); logger.error(message + ": " + e.getMessage()); event.markRollBack(); throw new ModelIntegrityClientException(message, e); } catch (Exception e) { logger.error("Failed to check document after creation", e); } } if (!DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName()) || sourceDocument.isProxy()) { DocumentModel removedProxyDocumentIfAny = null; if (DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName())/* && sourceDocument.isProxy()*/) { // (TODO necessarily live) Proxy deleted, update the true document, TODO if it is live removedProxyDocumentIfAny = sourceDocument; sourceDocument = documentManager.getSourceDocument(removedProxyDocumentIfAny.getRef()); if (sourceDocument.isVersion()) { return; // since source document is version, can't modify it ; but no need either, // because in any way proxy had been created after the version was } documentModified = false; // TODO reset documentModified for now, may change again below } // Update parents info if (!DocumentEventTypes.DOCUMENT_UPDATED.equals(event.getName()) && (sourceDocument.isProxy() || removedProxyDocumentIfAny != null || // nothing to do if actual doc just created : !DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName()) && !DocumentEventTypes.DOCUMENT_CREATED_BY_COPY.equals(event.getName()))) { try { documentModified = updateParentIdsMetadata(documentManager, documentService, sourceDocument, removedProxyDocumentIfAny) || documentModified; } catch (Exception e) { logger.error("Failed to maintain parents information", e); } } Set<String> inheritedFacets = metamodelService.getInheritedFacets(sourceDocument.getFacets()); if (!inheritedFacets.isEmpty()) { try { // Copy metadata from inherited facets to children if (DocumentEventTypes.DOCUMENT_UPDATED.equals(event.getName())) { metamodelService.applyFacetInheritance(documentManager, sourceDocument, true); // NB. isFacetSource = true so doesn't change the sourceDocument itself } else { // Reset metadata after move/deletion if (DocumentEventTypes.DOCUMENT_MOVED.equals(event.getName()) || DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName())) { // reset metadata if document is target of metadata inheritance (transfer.to) documentModified = metamodelService.resetInheritedFacets(sourceDocument) || documentModified; if (documentModified) { documentManager.saveDocument(sourceDocument); documentModified = false; } } // Copy metadata from inherited facets from parents if (!DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName())) { metamodelService.applyFacetInheritance(documentManager, sourceDocument, false); if (DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName()) || DocumentEventTypes.DOCUMENT_MOVED.equals(event.getName())) { if (documentModified) { documentManager.saveDocument(sourceDocument); documentModified = false; } } } } } catch (Exception e) { logger.error("Failed to manage inherited facets of an SoaNode", e); } } } try { boolean isDirty = sourceDocument.isDirty(); boolean isProxy = sourceDocument.isProxy(); if (documentModified || isDirty && isProxy) { // NB. !isDirty happens when handleEvent loops (an RML save itself triggers a save) // and isDirty && !documentModified happens on a just created document (or proxy), must be saved else inherited facet metadata won't be updated when child (proxy) moved if (!sourceDocument.isDirty()) { logger.warn("RepositoryManagerListener : sourceDocument modified but not dirty ?!?"); return; } ///sourceDocument.getContextData().putScopedValue(ScopeType.REQUEST, /// CONTEXT_REQUEST_REPOSITORY_ALREADY_MANAGED, true); // preventing loop from inherited facets at UPDATE in ex. DiscoveryServiceTest documentManager.saveDocument(sourceDocument); } //logger.debug("RepositoryManagerListener : Doing final save (for updateParentIdsMetadata ?)"); } catch (Exception e) { logger.error("RepositoryManagerListener : Failed to do final save (for updateParentIdsMetadata ?)", e); } } /** * Saves sourceDocument only if deleted after merge with corresponding existing document, * otherwise may move it but in itself doesn't change it (?). * Saves session if repository structure changed. * @param documentManager * @param documentService * @param sourceDocument * @return * @throws ClientException * @throws PropertyException * @throws Exception */ private DocumentModel manageRepositoryStructure(CoreSession documentManager, DocumentService documentService, DocumentModel sourceDocument, int repositoryManagementLoopCount) throws ClientException, PropertyException, Exception { boolean structureChanged = false; // If a document has been created through the Nuxeo UI, move it to the repository and leave only a proxy DocumentModel parentModel = documentManager.getDocument(sourceDocument.getParentRef()); String sourceRepositoryPath = RepositoryHelper.getRepositoryPath(documentManager, sourceDocument); // NB. if proxy, may be of a different subproject than parent, but anyway its place is in its parent's String sourceFolderPath = documentService.getSourceFolderPathBelowSubprojectRepository( RepositoryHelper.getRepositoryPath(documentManager, sourceDocument), sourceDocument.getType()); DocumentModel parentParentModel = documentManager.getParentDocument(documentManager.getParentDocument(sourceDocument.getRef()).getRef()); SoaNodeId soaNodeId = documentService.createSoaNodeId(sourceDocument); // if it's not a proxy if (!sourceDocument.isProxy() ///&& !(sourceDocument.getType().equals(parentModel.getName()) // TODO RepositoryHelper.isRepository() ///&& Repository.DOCTYPE.equals(parentParentModel.getType())) // or if it's a proxy not in the Repository but having just been copied below a proxy of SoaNode (ex. TaggingFolder)... || sourceDocument.isProxy() && parentModel.hasSchema(SoaNode.SCHEMA) && parentModel.isProxy() && !sourceDocument.getPathAsString().startsWith(sourceRepositoryPath)) { DocumentModel repositoryDocument = documentService.findSoaNode(documentManager, soaNodeId); if (repositoryDocument != null && repositoryDocument.getPath().toString().startsWith(sourceRepositoryPath)) { // TODO else ???? & false (see above) if (!repositoryDocument.getRef().equals(sourceDocument.getRef()) // also equals if one proxies the other ?!?! && !sourceDocument.isProxy()) { // if proxy, no differences and nothing to do // If there is already a corresponding repositoryDocument, merge and only keep one repositoryDocument.copyContent(sourceDocument); repositoryDocument.getContextData().putScopedValue(ScopeType.REQUEST, CONTEXT_REQUEST_REPOSITORY_MANAGEMENT_LOOP_COUNT, repositoryManagementLoopCount); documentManager.saveDocument(repositoryDocument); // may trigger another event documentManager.removeDocument(sourceDocument.getRef()); // If it's not already in its type's default Repository folder, if (!parentModel.getPathAsString().equals(sourceFolderPath)) { // Create a proxy at the expected location if (parentModel.isProxy() && documentService.isSoaNode(documentManager, parentModel.getType())) { // make sure parent SOA node is not a proxy (may happen also in the second case) parentModel = documentService.findSoaNode(documentManager, documentService.createSoaNodeId(parentModel)); // NB. ?? Nuxeo UI requires also a proxy below the parent proxy, but getChild(parent, repositoryDocument.getName()) can't get it ?! /*try { if (documentManager.getChild(parentModel.getRef(), repositoryDocument.getName()) != null) { // if none yet // creating a proxy under the right, non-proxy parent sourceDocument = documentManager.createProxy(repositoryDocument.getRef(), parentModel.getRef()); } } catch (Exception e) { // happens if no child }*/ } sourceDocument = documentManager.createProxy(repositoryDocument.getRef(), parentModel.getRef()); } structureChanged = false; } else if (!parentModel.getPathAsString().equals(sourceFolderPath) && sourceDocument.isProxy() && parentModel.isProxy() && documentService.isSoaNode(documentManager, parentModel.getType())) { documentService.getSourceFolder(documentManager, sourceDocument); // ensuring it exists // if both proxies, move child under its parent's repository document (happens ?!?) DocumentModel actualParentModel = documentService.findSoaNode(documentManager, documentService.createSoaNodeId(parentModel)); sourceDocument = documentManager.move(sourceDocument.getRef(), actualParentModel.getRef(), sourceDocument.getName()); // NB. stays the same doc structureChanged = true; } // else already at right place : nothing to do } else if (!parentModel.getPathAsString().equals(sourceFolderPath)) { // Move to (the sourceDocument's) repository otherwise documentService.getSourceFolder(documentManager, sourceDocument); // ensuring it exists repositoryDocument = documentManager.move(sourceDocument.getRef(), new PathRef(sourceFolderPath), sourceDocument.getName()); // NB. stays the same doc // NB. save required after move before creating proxy (?!) // Create a proxy at the expected location if (parentModel.isProxy() && documentService.isSoaNode(documentManager, parentModel.getType())) { // make sure parent SOA node is not a proxy (may happen also in the second case) parentModel = documentService.findSoaNode(documentManager, documentService.createSoaNodeId(parentModel)); } sourceDocument = documentManager.createProxy(repositoryDocument.getRef(), parentModel.getRef()); structureChanged = true; } // else already at right place } if (structureChanged) { documentManager.save(); } return sourceDocument; } private boolean updateParentIdsMetadata(CoreSession documentManager, DocumentService documentService, DocumentModel sourceDocument, DocumentModel removedProxyDocumentIfAny) throws Exception { SoaNode sourceSoaNode = sourceDocument.getAdapter(SoaNode.class); List<DocumentModel> parentModels = documentService.findAllParents(documentManager, sourceDocument); boolean changed = false; DocumentModelList soaNodeParentModels = new DocumentModelListImpl(); Iterator<SoaNodeId> oldParentIdIt = sourceSoaNode.getParentIds().iterator(); for (DocumentModel parentModel : parentModels) { if (documentService.isSoaNode(documentManager, parentModel.getType())) { if (removedProxyDocumentIfAny != null && parentModel.getRef().equals(removedProxyDocumentIfAny.getParentRef())) { // don't re-add it ! happens on ABOUT_TO_REMOVE event because findAllParents() still // returns proxyDocumentIfAny because it's not yet been removed changed = true; } else { soaNodeParentModels.add(parentModel); if (!changed && (!oldParentIdIt.hasNext() || !parentModel.equals(oldParentIdIt.next()))) { changed = true; } } } } if (changed) { sourceSoaNode.setParentIds(documentService.createSoaNodeIds(soaNodeParentModels.toArray(new DocumentModel[]{}))); } return changed; } }