/****************************************************************************** * Copyright (c) 2011-2013, Linagora * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Linagora - initial API and implementation *******************************************************************************/ package com.ebmwebsourcing.petals.services.eip.designer.helpers; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import com.ebmwebsourcing.petals.common.generation.Mep; import com.ebmwebsourcing.petals.common.internal.provisional.utils.NamespaceUtils; import com.ebmwebsourcing.petals.common.internal.provisional.utils.StringUtils; import com.ebmwebsourcing.petals.common.internal.provisional.utils.XPathUtils; import com.ebmwebsourcing.petals.services.eip.designer.model.AbstractNode; import com.ebmwebsourcing.petals.services.eip.designer.model.EIPtype; import com.ebmwebsourcing.petals.services.eip.designer.model.EipChain; import com.ebmwebsourcing.petals.services.eip.designer.model.EipConnection; import com.ebmwebsourcing.petals.services.eip.designer.model.EipNode; import com.ebmwebsourcing.petals.services.eip.designer.model.EipProperty; import com.ebmwebsourcing.petals.services.eip.designer.model.EipProperty.EipPropertyType; import com.ebmwebsourcing.petals.services.eip.designer.model.Endpoint; import com.ebmwebsourcing.petals.services.explorer.model.EndpointBean; import com.ebmwebsourcing.petals.services.utils.ConsumeUtils; /** * A class in charge of maintaining results of an EIP chain validation. * @author Vincent Zurczak - EBM WebSourcing * FIXME: once internationalized, the strings will take much less space */ public final class EipChainTransactionalValidator { private final Map<AbstractNode,List<String>> nodeToErrorMessage; private final Map<EipConnection,List<String>> connectionToErrorMessage; private final Map<EipConnection,List<String>> connectionToWarningMessage; /** * Constructor. */ public EipChainTransactionalValidator() { this.nodeToErrorMessage = new HashMap<AbstractNode,List<String>> (); this.connectionToErrorMessage = new HashMap<EipConnection,List<String>> (); this.connectionToWarningMessage = new HashMap<EipConnection, List<String>>(); } /** * Validates all the elements of this chain. * @param eipChain */ public void validateAll( EipChain eipChain ) { // Clear the contextual messages this.nodeToErrorMessage.clear(); this.connectionToErrorMessage.clear(); this.connectionToWarningMessage.clear(); // Validate the properties of connections first // Nodes also use connections properties and add other errors about connections for( EipConnection conn : eipChain.getConnections()) { validateConnection( conn ); } // Prepare the check for the end-point uniqueness in the chain Map<EndpointBean,List<AbstractNode>> edptToNodes = new HashMap<EndpointBean,List<AbstractNode>> (); // Validate the properties of the EIP nodes Map<String,List<EipNode>> eipServiceNameToEip = new HashMap<String,List<EipNode>> (); for( EipNode eip : eipChain.getEipNodes()) { validateEipNode( eip ); // Valid node => we have an EIP List<String> messages = this.nodeToErrorMessage.get( eip ); if( messages == null || messages.isEmpty()) { EndpointBean bean = new EndpointBean(); bean.setEndpointName( eip.getEndpointName()); bean.setInterfaceName( new QName( eip.getInterfaceNamespace(), eip.getInterfaceName())); bean.setServiceName( new QName( eip.getServiceNamespace(), eip.getServiceName())); // This is to check the end-point uniqueness (itf + srv + edpt) List<AbstractNode> list = edptToNodes.get( bean ); if( list == null ) list = new ArrayList<AbstractNode> (); list.add( eip ); edptToNodes.put( bean, list ); // This is to check the uniqueness of the service name (local part) List<EipNode> eips = eipServiceNameToEip.get( eip.getServiceName()); if( eips == null ) eips = new ArrayList<EipNode> (); eips.add( eip ); eipServiceNameToEip.put( eip.getServiceName(), eips ); } } // We must also avoid a same service name for EIPs, because of the export (can lead to conflict during the export). // This is a constraint due to the fact we have one project / SU per EIP. for( Map.Entry<String,List<EipNode>> entry : eipServiceNameToEip.entrySet()) { if( entry.getValue().size() == 1 ) continue; for( EipNode eip : entry.getValue()) addErrorMessage( eip, "Same service names are not allowed within a same chain. " + entry.getKey() + " is already used." ); } // Validate the properties of the end-points for( Endpoint edpt : eipChain.getEndpoints()) { validateEndpoint( edpt ); // Valid node => we have an end-point List<String> messages = this.nodeToErrorMessage.get( edpt ); if( messages == null || messages.isEmpty()) { EndpointBean bean = new EndpointBean(); bean.setEndpointName( edpt.getEndpointName()); bean.setInterfaceName( new QName( edpt.getInterfaceNamespace(), edpt.getInterfaceName())); bean.setServiceName( new QName( edpt.getServiceNamespace(), edpt.getServiceName())); List<AbstractNode> list = edptToNodes.get( bean ); if( list == null ) list = new ArrayList<AbstractNode> (); list.add( edpt ); edptToNodes.put( bean, list ); } } // Check the end-point uniqueness in the chain for( List<AbstractNode> nodes : edptToNodes.values()) { if( nodes.size() == 1 ) continue; // Two EIP cannot have the same ID. // An EIP and an end-point cannot either. // Two end-points with the same ID refer to the same end-point. int eipCpt = 0; boolean hasEdpt = false; for( AbstractNode node : nodes ) { if( node instanceof EipNode ) eipCpt ++; else if( node instanceof Endpoint ) hasEdpt = true; } // Add the errors if( hasEdpt && eipCpt > 0 ) { for( AbstractNode node : nodes ) { if( node instanceof EipNode ) addErrorMessage( node, "This EIP identifier is already taken by a Petals service used in this chain." ); } } else if (eipCpt > 1 ) { for( AbstractNode node : nodes ) { if( node instanceof EipNode ) addErrorMessage( node, "This EIP identifier is already used by another EIP in the chain." ); } } } // Update the model with the found error messages for( Map.Entry<AbstractNode,List<String>> entry : this.nodeToErrorMessage.entrySet()) { entry.getKey().setErrorMessages( entry.getValue()); } for( Map.Entry<EipConnection,List<String>> entry : this.connectionToErrorMessage.entrySet()) { entry.getKey().setErrorMessages( entry.getValue()); } for (Map.Entry<EipConnection,List<String>> entry : this.connectionToWarningMessage.entrySet()) { entry.getKey().setWarningMessages( entry.getValue()); } } /** * @return the nodeToErrorMessage */ public Map<AbstractNode,List<String>> getNodeToErrorMessage() { return this.nodeToErrorMessage; } /** * @return the connectionToErrorMessage */ public Map<EipConnection,List<String>> getConnectionToErrorMessage() { return this.connectionToErrorMessage; } /** * @return the warningMessages */ public Map<EipConnection,List<String>> getConnectionToWarningMessage() { return this.connectionToWarningMessage; } /** * @param node the node that was validated * @param errorMessage an error message (not null) */ private void addErrorMessage( AbstractNode node, String errorMessage ) { List<String> messages = this.nodeToErrorMessage.get( node ); if( messages == null ) { messages = new ArrayList<String> (); this.nodeToErrorMessage.put( node, messages ); } messages.add( errorMessage ); } /** * @param conn the connection that was validated * @param errorMessage an error message (not null) */ private void addErrorMessage( EipConnection conn, String errorMessage ) { List<String> messages = this.connectionToErrorMessage.get( conn ); if( messages == null ) { messages = new ArrayList<String> (); this.connectionToErrorMessage.put( conn, messages ); } messages.add( errorMessage ); } /** * @param conn the connection that was validated * @param warningMessage an error message (not null) */ private void addWarningMessage( EipConnection conn, String warningMessage ) { List<String> messages = this.connectionToWarningMessage.get( conn ); if( messages == null ) { messages = new ArrayList<String> (); this.connectionToWarningMessage.put( conn, messages ); } messages.add( warningMessage ); } /** * Clears all the error messages for this connection. * @param conn */ private void clearErrorMessages( EipConnection conn ) { List<String> messages = this.connectionToErrorMessage.get( conn ); if( messages == null ) messages = new ArrayList<String> (); else messages.clear(); this.connectionToErrorMessage.put( conn, messages ); } /** * Clears all the error messages for this connection. * @param conn */ private void clearWarningMessages( EipConnection conn ) { List<String> messages = this.connectionToWarningMessage.get( conn ); if( messages == null ) messages = new ArrayList<String> (); else messages.clear(); this.connectionToWarningMessage.put( conn, messages ); } /** * Clears all the error messages for this node. * @param node */ private void clearErrorMessages( AbstractNode node ) { List<String> messages = this.nodeToErrorMessage.get( node ); if( messages == null ) messages = new ArrayList<String> (); else messages.clear(); this.nodeToErrorMessage.put( node, messages ); } /** * Validates the properties of an EIP node. * @param eip */ private void validateEipNode( EipNode eip ) { // Clear messages clearErrorMessages( eip ); // Service validation if( StringUtils.isEmpty( eip.getServiceName()) != StringUtils.isEmpty( eip.getServiceNamespace())) addErrorMessage( eip, "A service name is a QName. Both the service local name and the service name space should be set (or none of them)." ); if( StringUtils.isEmpty( eip.getInterfaceName()) != StringUtils.isEmpty( eip.getInterfaceNamespace())) addErrorMessage( eip, "An interface name is a QName. Both the interface local name and the interface name space should be set (or none of them)." ); if( StringUtils.isEmpty( eip.getInterfaceName())) addErrorMessage( eip, "The interface name is required." ); if( StringUtils.isEmpty( eip.getServiceName())) addErrorMessage( eip, "The service name is required." ); if( StringUtils.isEmpty( eip.getEndpointName())) addErrorMessage( eip, "The end-point name is required." ); // Validate XPath properties // Router conditions are on the connections, not in the EIP properties for( Map.Entry<EipProperty,String> entry : eip.getProperties().entrySet()) { if( entry.getKey().getType() == EipPropertyType.XPATH ) { String msg = XPathUtils.validateXPathExpression( entry.getValue()); if( msg != null ) addErrorMessage( eip, msg ); } } // TODO: for the routing-slip, the invocation pattern should be the one of its last target // TODO: for routers and dynamic routers, the MEP should be the same for all the EIP and all the targets // TODO: think about the other patterns and the MEP coherence... // EIP validation String msg = null; int outCpt = eip.getOutgoingConnections().size(); switch( eip.getEipType()) { case AGGREGATOR: if( outCpt != 1 ) { msg = "The " + eip.getEipType().toString() + " pattern only supports 1 connected service."; addErrorMessage( eip, msg ); } break; case BRIDGE: if( outCpt != 1 ) { msg = "The " + eip.getEipType().toString() + " pattern only supports 1 connected service."; addErrorMessage( eip, msg ); } break; case DISPATCHER: if( outCpt < 1 ) { msg = "The " + eip.getEipType().toString() + " pattern requires at least 1 connected service."; addErrorMessage( eip, msg ); } if( ! isValidMep( eip.getIncomingConnection(), Mep.IN_ONLY )) addErrorMessage( eip.getIncomingConnection(), "A " + eip.getEipType().toString() + " pattern can only be invoked with the " + Mep.IN_ONLY + " MEP." ); for( EipConnection conn : eip.getOutgoingConnections()) { if( ! isValidMep( conn, Mep.IN_ONLY )) addErrorMessage( conn, "A " + eip.getEipType().toString() + " pattern can only invoke operations with the " + Mep.IN_ONLY + " MEP." ); } break; case DYNAMIC_ROUTER: if( outCpt < 3 ) { msg = "The pattern " + eip.getEipType().toString() + " is useless if there are less than 3 connected services."; addErrorMessage( eip, msg ); } else if( ! isValidMep( eip.getOutgoingConnections().get( 0 ), Mep.IN_OUT )) { addErrorMessage( eip.getOutgoingConnections().get( 0 ), "The first connected service of a " + eip.getEipType().toString() + " pattern must be invokek with the " + Mep.IN_OUT + " MEP." ); } break; case ROUTER: if( outCpt < 2 ) { msg = "The pattern " + eip.getEipType().toString() + " is useless if there are less than 2 connected services."; addErrorMessage( eip, msg ); } break; case ROUTING_SLIP: if( outCpt < 1 ) { msg = "The " + eip.getEipType().toString() + " pattern requires at least 1 connected service."; addErrorMessage( eip, msg ); } else { int size = eip.getOutgoingConnections().size(); for( int i=0; i<size-1; i++ ) { if( ! isValidMep( eip.getOutgoingConnections().get( i ), Mep.IN_OUT )) addErrorMessage( eip.getOutgoingConnections().get( i ), "This service must be invoked with the " + Mep.IN_OUT + " MEP." ); } if( eip.getIncomingConnection() != null ) { String mep = eip.getIncomingConnection().getConsumeMep(); Mep expectedMep = Mep.whichMep( mep ); if( ! isValidMep( eip.getOutgoingConnections().get( size-1 ), expectedMep )) addErrorMessage( eip.getOutgoingConnections().get( size-1 ), "This service must be invoked using the " + expectedMep + " MEP to be coherent with the original request." ); } } break; case SCATTER_GATHER: if( outCpt < 1 ) { msg = "The " + eip.getEipType().toString() + " pattern requires at least 1 connected service."; addErrorMessage( eip, msg ); } if( ! isValidMep( eip.getIncomingConnection(), Mep.IN_OUT )) addErrorMessage( eip.getIncomingConnection(), "A " + eip.getEipType().toString() + " pattern can only be invoked with the " + Mep.IN_OUT + " MEP." ); break; case SPLITTER: if( outCpt != 1 ) { msg = "The " + eip.getEipType().toString() + " pattern only supports 1 connected service."; addErrorMessage( eip, msg ); } if( ! isValidMep( eip.getIncomingConnection(), Mep.IN_OUT )) addErrorMessage( eip.getIncomingConnection(), "A " + eip.getEipType().toString() + " pattern can only be invoked with the " + Mep.IN_OUT + " MEP." ); break; case WIRETAP: if( outCpt != 2 ) { msg = "The " + eip.getEipType().toString() + " pattern requires exactly 2 connected services."; addErrorMessage( eip, msg ); } if( StringUtils.isEmpty( eip.getProperties().get( EipProperty.WIRETAP_WAY ))) addErrorMessage( eip, "The wiretap way is not set." ); if( ! isValidMep( eip.getOutgoingConnections().get( 1 ), Mep.IN_ONLY )) addErrorMessage( eip.getOutgoingConnections().get( 1 ), "The monitoring flow must be invoked with the " + Mep.IN_ONLY + " MEP." ); break; } } /** * Validates the properties of an end-point. * @param edpt */ private void validateEndpoint( Endpoint edpt ) { // Clear messages clearErrorMessages( edpt ); // End-point validation if( StringUtils.isEmpty( edpt.getServiceName()) != StringUtils.isEmpty( edpt.getServiceNamespace())) addErrorMessage( edpt, "A service name is a QName. Both the service local name and the service name space should be set (or none of them)." ); if( StringUtils.isEmpty( edpt.getInterfaceName()) != StringUtils.isEmpty( edpt.getInterfaceNamespace())) addErrorMessage( edpt, "An interface name is a QName. Both the interface local name and the interface name space should be set (or none of them)." ); if( StringUtils.isEmpty( edpt.getInterfaceName())) addErrorMessage( edpt, "The interface name is required." ); // End-point's specific validation if( edpt.getIncomingConnection() == null ) addErrorMessage( edpt, "This service will never be invoked (no incoming connection)." ); } /** * Validates the connection. * <p> * Error messages should be stored with {@link EipChainTransactionalValidator#addErrorMessage(AbstractNode, String)}. * </p> * * @param validator a validator (not null) * <p> * The validator will validate a part or the entire chain. * Once it has all the errors, it updates the model, which triggers a property change. * </p> * <p> * Even connections with no error should be added in the validator. Use * <code>validator.addErrorMessage( conn, null )</code> * </p> */ private void validateConnection( EipConnection conn ) { clearErrorMessages( conn ); clearWarningMessages(conn); // Validate conditions String msg = null; if( conn.shouldHaveCondition()) { if( StringUtils.isEmpty( conn.getConditionExpression())) msg = "A condition must be defined."; else { if( conn.getSource().getEipType() == EIPtype.ROUTER && EipProperty.ROUTING_CRITERIA_BY_OPERATION.equals( conn.getSource().getProperties().get( EipProperty.ROUTING_CRITERIA ))) { if( ! NamespaceUtils.isShortenNamespace( conn.getConditionExpression())) msg = "The routing operation is not a valid QName."; } else { msg = XPathUtils.validateXPathExpression( conn.getConditionExpression()); } } } if( msg != null ) { addErrorMessage( conn, msg ); } // Validate operation and MEP - they must be set if( StringUtils.isEmpty( conn.getConsumeOperation())) { addWarningMessage( conn, "The operation to invoke is not defined." ); } else if( ! NamespaceUtils.isShortenNamespace( conn.getConsumeOperation())) { addErrorMessage( conn, "The operation to invoke must be a valid qualified name." ); } if( StringUtils.isEmpty( conn.getConsumeMep()) || Mep.UNKNOWN.toString().equals( conn.getConsumeMep())) { addWarningMessage( conn, "The invocation pattern (MEP) should be set." ); } // Validate the MEP and the invoked operations for the end-points // Skip this step if the operation is not set else if( conn.getTarget() instanceof Endpoint && ! StringUtils.isEmpty( conn.getConsumeOperation())) { Mep currentMep = Mep.whichMep( conn.getConsumeMep()); QName currentOperation = NamespaceUtils.buildQName( conn.getConsumeOperation()); AbstractNode t = conn.getTarget(); QName itfName = null; if( ! StringUtils.isEmpty( t.getInterfaceName()) && ! StringUtils.isEmpty( t.getInterfaceNamespace())) itfName = new QName( t.getInterfaceNamespace(), t.getInterfaceName()); QName srvName = null; if( ! StringUtils.isEmpty( t.getServiceName()) && ! StringUtils.isEmpty( t.getServiceNamespace())) srvName = new QName( t.getServiceNamespace(), t.getServiceName()); String edptName = t.getEndpointName(); if( StringUtils.isEmpty( edptName )) edptName = null; Map<QName,Mep> consumableOps = ConsumeUtils.getValidOperationsForConsume( itfName, srvName, edptName ); if( ! consumableOps.containsKey( currentOperation )) { addWarningMessage( conn, "The invoked operation is not available for this service." ); } else if( currentMep != Mep.UNKNOWN && consumableOps.get( currentOperation ) != currentMep ) { addWarningMessage( conn, "The invocation pattern (MEP) does not match the operation's one (or there is an ambiguity in the service consumption)." ); } } } /** * Compares the MEP associated with a connection and an expected MEP. * @param conn the connection to check (can be null) * @param expectedMep the expected MEP (not null) * @return true if the expected MEP and the associated one are equal or if the connection is null, false otherwise */ private boolean isValidMep( EipConnection conn, Mep expectedMep ) { return conn == null || expectedMep.toString().equalsIgnoreCase( conn.getConsumeMep()); } }