package org.easysoa.registry; import java.io.Serializable; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.log4j.Logger; import org.easysoa.registry.types.InformationService; import org.easysoa.registry.types.ServiceImplementation; import org.easysoa.registry.types.SoaNode; import org.easysoa.registry.types.Subproject; import org.easysoa.registry.types.SubprojectNode; import org.easysoa.registry.types.ids.SoaNodeId; import org.easysoa.registry.utils.NuxeoListUtils; import org.nuxeo.common.utils.IdUtils; 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.model.PropertyException; import org.nuxeo.runtime.api.Framework; /** * Computes... * * Steps : * * matchingFirst : if soaname is prefixed with "matchingFirst:", looks up the discovered node * using the matching algorithm (ex. on wsdlPortTypeName & platform metas) * * findSoaNode : if none yet, finds it using SOA node ID (exact match) * * createOrUpdate : * * linkToParentDocuments : * * still TODO (see FIXMEs) : * if needed soaMetamodel performances * special cases (see FIXMEs) * * @author mkalam-alami, mdutoo * */ public class DiscoveryServiceImpl implements DiscoveryService { private static Logger logger = Logger.getLogger(DiscoveryServiceImpl.class); public DocumentModel runDiscovery(CoreSession documentManager, SoaNodeId identifier, Map<String, Object> properties, List<SoaNodeId> parentDocuments) throws Exception { DocumentService documentService = Framework.getService(DocumentService.class); ServiceMatchingService serviceMatchingService = Framework.getService(ServiceMatchingService.class); EndpointMatchingService endpointMatchingService = Framework.getService(EndpointMatchingService.class); if (!documentService.isSoaNode(documentManager, identifier.getType())) { throw new Exception("Can only discover a SoaNode but is " + identifier + " - " + properties + " - " + parentDocuments); } // find subproject (or create default one), from SOA node id or properties : String subproject = identifier.getSubprojectId(); if ((subproject == null || subproject.trim().length() == 0) && properties != null) { subproject = (String) properties.get(SubprojectNode.XPATH_SUBPROJECT); if (subproject == null || subproject.trim().length() == 0) { // default subproject case subproject = Subproject.DEFAULT_SUBPROJECT_ID; properties.put(SubprojectNode.XPATH_SUBPROJECT, subproject); // else same nodes // can't be merged, ex. several identical endpoints created by dbb ! } identifier.setSubprojectId(subproject); } // else don't create properties, otherwise when called with null to create links // (EndpointMatchingService.linkServiceImplementation()), will be saved which triggers loop DocumentModel subprojectDocModel = SubprojectServiceImpl .getSubprojectOrCreateDefault(documentManager, subproject); if (subprojectDocModel == null) { throw new Exception("EasySOA DiscoveryService : can't find given subproject " + subproject + " (create it first), aborting discovery of " + identifier); } // Fetch or create document DocumentModel newDocumentModel = null; DocumentModel foundDocumentModel = null; Map<String, Serializable> nuxeoProperties = toNuxeoProperties(properties); // matching first mode : // (for nodes that the probe does't control but wants to refer to, ex. InformationService in source disco) boolean matchFirst = identifier.getName() == null || identifier.getName().length() == 0 || identifier.getName().startsWith("matchFirst:"); int matchingFirstCandidateNb = 0; if (matchFirst) { // TODO extract to API // TODO or == null ??!?? (NOO) // try to find an existing one that matches on at least one exact prop // first, removing "implicit:" prefix (else newDocumentModel, which may be created if none found, will have it) identifier = new SoaNodeId(identifier.getSubprojectId(), identifier.getType(), identifier.getName().substring(11)); // create temporary model as a support to matching : newDocumentModel = documentService.newSoaNodeDocument(documentManager, identifier, nuxeoProperties); SubprojectServiceImpl.copySubprojectNodeProperties(subprojectDocModel, newDocumentModel); // finding existing through maching : DocumentModelList matchingFirstResults = null; String filterComponentId = null; //TODO ?? if (documentService.isTypeOrSubtype(documentManager, identifier.getType(), InformationService.DOCTYPE)) { matchingFirstResults = serviceMatchingService.findInformationServices(documentManager, newDocumentModel, filterComponentId, false, true); } else if (documentService.isTypeOrSubtype(documentManager, identifier.getType(), ServiceImplementation.DOCTYPE)) { matchingFirstResults = endpointMatchingService.findServiceImplementations(documentManager, newDocumentModel, filterComponentId, false, true); } matchingFirstCandidateNb = matchingFirstResults.size(); if (matchingFirstCandidateNb == 1) { DocumentModel matchingSoaNode = matchingFirstResults.get(0); identifier = documentService.createSoaNodeId(matchingSoaNode); foundDocumentModel = matchingSoaNode; } } if (foundDocumentModel == null) { // not matchFirst or no (or too many) match found // finding existing using SOA node ID : // (search in exact subproject / Phase, else allowing several SOA IDs (in different // subprojects / Phases) for a single node ; at worse it should create an "inheriting" one) foundDocumentModel = documentService.findSoaNode(documentManager, identifier); } if (foundDocumentModel != null && foundDocumentModel.isVersion()) { // exists but is readonly version : do nothing (else triggers SQLDocumentVersion$VersionNotModifiableException) // TODO LATER maybe find a way to still provide info, such as adding a "code-level service layer" between iserv & serviceimpl ?? return foundDocumentModel; } if (matchingFirstCandidateNb > 1) { // don't know which one but at least there are candidates // therefore don't create it, else impl wouldn't know which one to match return null; } DocumentModel documentModel = null; documentModel = createOrUpdate(documentManager, documentService, foundDocumentModel, newDocumentModel, identifier, nuxeoProperties, matchFirst); linkToParentDocuments(documentManager, documentService, identifier, parentDocuments); // TODO also link to other known model elements, especially impl->IS ex. in source disco return documentModel; } /** * Static because used from EndpointMatchingService TODO better * @param documentManager * @param documentService * @param foundDocumentModel * @param newDocumentModel * @param identifier * @param nuxeoProperties * @param dontOverrideProperties * @return * @throws Exception */ public static DocumentModel createOrUpdate(CoreSession documentManager, DocumentService documentService, DocumentModel foundDocumentModel, DocumentModel newDocumentModel, SoaNodeId identifier, Map<String, Serializable> nuxeoProperties, boolean dontOverrideProperties) throws Exception { if (foundDocumentModel == null) { if (newDocumentModel == null) { newDocumentModel = documentService.newSoaNodeDocument(documentManager, identifier, nuxeoProperties); } // only now that props are OK, create or save document // (required below for handling parents by creating proxies to it) return documentManager.createDocument(newDocumentModel); } else { SoaMetamodelService metamodelService = Framework.getService(SoaMetamodelService.class); metamodelService.validateWriteRightsOnProperties(foundDocumentModel, nuxeoProperties); boolean changed = setNuxeoPropertiesIfChanged(foundDocumentModel, nuxeoProperties, dontOverrideProperties); if (changed) { // TODO or isDirty ?! if (!foundDocumentModel.isDirty()) { logger.error("DiscoveryServiceImpl : SAVING CHANGED BUT NOT DIRTY " + foundDocumentModel); } // only now that props are OK, create or save document // (required below for handling parents by creating proxies to it) // don't save if properties null, else triggers event loop with matching // (findAndMatchServiceImplementation, linkInformationServiceThroughPlaceholder, // linkServiceImplementation, runDiscovery) foundDocumentModel = documentManager.saveDocument(foundDocumentModel); } return foundDocumentModel; } } public static boolean setNuxeoPropertiesIfChanged(DocumentModel documentModel, Map<String, Serializable> nuxeoProperties, boolean dontOverrideProperties) throws PropertyException, ClientException { if (nuxeoProperties == null) { return false; } boolean changed = false; for (Entry<String, Serializable> nuxeoProperty : nuxeoProperties.entrySet()) { String propertyKey = nuxeoProperty.getKey(); Serializable propertyValue = nuxeoProperty.getValue(); changed = setPropertyIfChanged(documentModel, propertyKey, (Serializable) propertyValue, dontOverrideProperties) || changed; // TODO or isDirty ?! } return changed; } public static boolean setPropertiesIfChanged(DocumentModel documentModel, Map<String, Object> properties, boolean dontOverrideProperties) throws PropertyException, ClientException { if (properties == null) { return false; } boolean changed = false; for (Entry<String, Object> property : properties.entrySet()) { String propertyKey = property.getKey(); Object propertyValue = property.getValue(); // FIXME Non-serializable error handling changed = setPropertyIfChanged(documentModel, propertyKey, (Serializable) propertyValue, dontOverrideProperties) || changed; // TODO or isDirty ?! } return changed; } private static boolean setPropertyIfChanged(DocumentModel documentModel, String propertyKey, Serializable propertyValue, boolean dontOverrideProperties) throws PropertyException, ClientException { Serializable docPropertyValue = documentModel.getPropertyValue(propertyKey); if (dontOverrideProperties && docPropertyValue != null // in dontOverrideProperties (ex.matchFirst mode), only set if doesn't exist yet || propertyEquals(docPropertyValue, propertyValue)) { // don't set if the same return false; } documentModel.setPropertyValue(propertyKey, (Serializable) propertyValue); return true; } /** * WARNING may be costly ! * Both must be in their Nuxeo better form (ex. String[] and not List...) * TODO better handle all cases of complex properties : types, depth, lists... * @param docPropertyValue * @param propertyValue * @return */ private static boolean propertyEquals(Serializable docPropertyValue, Object propertyValue) { if (docPropertyValue == null && propertyValue == null) { return true; } if (docPropertyValue != null) { if (docPropertyValue instanceof String[]) { // FIXME better handle all cases of complex properties : types, depth, lists, maps (for operations)... if (propertyValue instanceof String[]) { return Arrays.deepEquals((String[]) docPropertyValue, (String[]) propertyValue); } else if (propertyValue instanceof List) { return Arrays.deepEquals((String[]) docPropertyValue, ((List<?>) propertyValue).toArray(NuxeoListUtils.EMPTY_STRING_ARRAY)); } else { return false;//TODO } } else {//TODO return docPropertyValue.equals(propertyValue); } } return false; } /** * Converts (so values stored in Nuxeo will be well typed) : * * Boolean to String * * List to String[] * @param properties * @return */ private Map<String, Serializable> toNuxeoProperties(Map<String, Object> properties) { if (properties == null) { return null; } Map<String, Serializable> nuxeoProperties = new HashMap<String, Serializable>(properties.size()); for (Entry<String, Object> property : properties.entrySet()) { // FIXME Non-serializable error handling Object propertyValue = property.getValue(); if (propertyValue instanceof Boolean) { propertyValue = propertyValue.toString(); }/* else if (propertyValue instanceof List) { propertyValue = ((List<?>) propertyValue).toArray(NuxeoListUtils.EMPTY_STRING_ARRAY); // TODO also non-String properties }*///TODO NOO rather array to list comparison in propertyEquals() nuxeoProperties.put(property.getKey(), (Serializable) propertyValue); } return nuxeoProperties; } private void linkToParentDocuments(CoreSession documentManager, DocumentService documentService, SoaNodeId identifier, List<SoaNodeId> parentDocuments) throws Exception { // FIXME cache / build model of soaMetamodelService responses to speed it up if (parentDocuments != null && !parentDocuments.isEmpty()) { String type = identifier.getType(); SoaMetamodelService soaMetamodelService = Framework.getService(SoaMetamodelService.class); for (SoaNodeId parentDocumentId : parentDocuments) { if (parentDocumentId.getSubprojectId() == null) { // if no subproject, put in same as child parentDocumentId.setSubprojectId(identifier.getSubprojectId()); } // else TODO soaMetamodelService supporting across subprojects List<String> pathBelowParent = soaMetamodelService.getPath(parentDocumentId.getType(), type); String parentPathAsString = null; if (parentDocumentId.getType() != null) { // Make sure parent is valid if (pathBelowParent == null) { documentManager.cancel(); throw new Exception("No possible valid path from " + parentDocumentId.getType() + " (" + parentDocumentId.toString() + ") to " + type + " (" + identifier.getName() + ")"); } else { parentPathAsString = createOrReuseIntermediateDocuments(documentManager, documentService, parentDocumentId, pathBelowParent, parentDocuments); if (parentPathAsString == null) { // parent document is version, so skip (because can't create anything in it) // TODO LATER redo parents through a reverse link allowing this case } } } else { // TODO when does it occur ?? parentPathAsString = parentDocumentId.getName(); } // Create target document below parent if necessary if (documentService.findProxy(documentManager, identifier, parentPathAsString) == null) { documentService.create(documentManager, identifier, parentPathAsString); } } } } /** * * @param documentManager * @param documentService * @param parentDocumentId * @param pathBelowParent * @param parentDocuments * @return null if parent document (with parentDocumentId) is a version (because then can't create anything below) * @throws ClientException */ private String createOrReuseIntermediateDocuments(CoreSession documentManager, DocumentService documentService, SoaNodeId parentDocumentId, List<String> pathBelowParent, List<SoaNodeId> parentDocuments) throws ClientException { DocumentModel parentDocument = documentService.findSoaNode(documentManager, parentDocumentId); // Create parent if necessary if (parentDocument == null) { parentDocument = documentService.create(documentManager, parentDocumentId); // NB. don't do a documentService.create() here, else triggers documentCreated event, even though // properties have not been set yet //parentDocument = documentService.newSoaNodeDocument(documentManager, parentDocumentId); } if (parentDocument.isVersion()) { // parent is readonly version : can't create path folders or tagging proxy, abort return null; } // Link the intermediate documents // If we have unknown documents between the two, create placeholders for (String pathStepType : pathBelowParent.subList(0, pathBelowParent.size() - 1)) { // Before creating a placeholder, check if the intermediate type // is not already listed in the parent documents boolean placeholderNeeded = true; for (SoaNodeId placeholderReplacementCandidate : parentDocuments) { if (pathStepType.equals(placeholderReplacementCandidate.getType())) { parentDocument = documentService.create(documentManager, placeholderReplacementCandidate, parentDocument.getPathAsString()); placeholderNeeded = false; break; } } if (placeholderNeeded) { parentDocument = documentService.create(documentManager, new SoaNodeId(parentDocumentId.getSubprojectId(), pathStepType, IdUtils.generateStringId()), parentDocument.getPathAsString()); parentDocument.setPropertyValue(SoaNode.XPATH_TITLE, "(Placeholder)"); parentDocument.setPropertyValue(SoaNode.XPATH_ISPLACEHOLDER, true); } } return parentDocument.getPathAsString(); } }