/** * Copyright (C) 2015 Orange * Licensed 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 com.francetelecom.clara.cloud.logicalmodel; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.validation.Valid; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.francetelecom.clara.cloud.commons.BusinessException; import com.francetelecom.clara.cloud.commons.GuiMapping; import com.francetelecom.clara.cloud.commons.TechnicalException; import com.francetelecom.clara.cloud.commons.UUIDUtils; import com.francetelecom.clara.cloud.commons.ValidatorUtil; /** * Logical Model for a deployment. * * This is the top level entry for the logical model. * * There is typically a single instance of a LogicalDeployment per * application release, which is used to describe parts that common to * all of its environments. * * @author APOG7416 */ @XmlRootElement @Entity @Table(name = "LOGICAL_DEPLOYMENT") public class LogicalDeployment extends LogicalModelItem { //See equals() for details private static final String[] EXCLUDED_HIBERNATE_BROKEN_EQUALS_COLLECTIONS = new String[] {"logicalServices", "processingNodes"}; public static final LogicalConfigServiceUtils LOGICAL_CONFIG_SERVICE_UTILS = new LogicalConfigServiceUtils(); /** * serialversion UID */ private static final long serialVersionUID = 8754768294184787020L; private static Logger logger=LoggerFactory.getLogger(LogicalDeployment.class.getName()); /** * ????. Mandatory????. * * Note: not currently used. This might be useful in the future for references * that the TechnicalModel has on the LogicalDeployment to indicate whether * the current instance is per Technical Model. */ @GuiMapping(status = GuiMapping.StatusType.SKIPPED) private boolean template = true; /** * When set to true, indicates the application described is subject to the Sarbanes�Oxley Act. See * http://en.wikipedia.org/wiki/Sarbanes%E2%80%93Oxley_Act As such specific requirement would apply (such as * collecting audit traces, archiving logs for a long duration, propagating the SOX flag to dependent services such * as DBaaS). */ @GuiMapping(status = GuiMapping.StatusType.SKIPPED) private boolean sox = false; /** * All logical services used by all deployment Node Cluster (aggregation). */ @XmlElementWrapper @XmlElement(name = "logicalService") @OneToMany(cascade = CascadeType.ALL, mappedBy = "logicalDeployment", orphanRemoval = true) @GuiMapping() @Valid protected List<LogicalService> logicalServices = new ArrayList<LogicalService>(); /** * All processing nodes in the deployment. */ @XmlElementWrapper @XmlElement(name = "processingNode") @OneToMany(cascade = CascadeType.ALL, mappedBy = "logicalDeployment", orphanRemoval = true) @GuiMapping() @Valid protected List<ProcessingNode> processingNodes = new ArrayList<ProcessingNode>(); /** * Default constructor. */ public LogicalDeployment() { super(); } /** * Public constructor. * * @param label * logical deployment label */ @Deprecated public LogicalDeployment(String label) { super(UUIDUtils.generateUUID("ld")); this.label = label; } /** * List all execution nodes . (Read-only) * @return an unmodifiable list of execution nodes sorted by type and label */ //TODO: return a Set instead of a List: no duplicates allowed in ExecNode. public List<ProcessingNode> listProcessingNodes() { return listProcessingNodes(null); } /** * List all execution nodes . (Read-only) * @param filteredType the class to filter execNode or null to return all nodes * @return an unmodifiable list of execution nodes sorted by type and label */ //TODO: return a Set instead of a List: no duplicates allowed in ExecNode. public <E extends ProcessingNode> List<E> listProcessingNodes(Class<E> filteredType) { if (filteredType == null) { List<ProcessingNode> nodes = new ArrayList<ProcessingNode>(this.processingNodes); Collections.sort(nodes); return (List<E>) Collections.unmodifiableList(nodes); } else { List<E> matchingNodes = new ArrayList<E>(); for (ProcessingNode executionNode : processingNodes) { if (filteredType.isInstance(executionNode)) { matchingNodes.add((E) executionNode); } } Collections.sort(matchingNodes); return Collections.unmodifiableList(matchingNodes); //overkill ? } } /** * Find a @{link JeeProcessing} by its name * @return */ public ProcessingNode findProcessingNodeByName(String name) { Validate.notEmpty(name, "unexpected empty or null name"); for (ProcessingNode node : processingNodes) { if ( node.getName().equals( name ) ) return node; } throw new IllegalArgumentException( "Unknown execution node " + name ); } /** * Find a @{link JeeProcessing} by its user-provided label * @return */ public ProcessingNode findProcessingNode(String label) { Validate.notEmpty(label, "unexpected empty or null label"); for (ProcessingNode node : processingNodes) { if ( node.getLabel().equals( label ) ) return node; } throw new IllegalArgumentException( "Unknown execution node " + label ); } /** * List all logical services. (Read-only) * FIXME: check if this should not return a Set instead * @return an unmodifiable list of logical services sorted by type and label */ public List<LogicalService> listLogicalServices() { List<LogicalService> services = new ArrayList<LogicalService>(this.logicalServices); Collections.sort(services); return Collections.unmodifiableList(services); } /** * Logical services filtered by type, and deduplicated. (Read-only), and filtered to match a given name. * @param filteredType the class to filter logical services or null to return all services * @param name an optional name/label to filter against {@link com.francetelecom.clara.cloud.logicalmodel.LogicalService#getName()} or {@link com.francetelecom.clara.cloud.logicalmodel.LogicalService#getLabel()} * or null to not perform any of this filtering * @return an unmodifiable list of logical services sorted by type and label */ public <E extends LogicalService> Set<E> listLogicalServices(Class<E> filteredType, String name) { Set<E> services = new TreeSet<E>(); for (LogicalService service : this.logicalServices) { boolean includeService = false; if (filteredType == null || filteredType.isInstance(service)) { if (name == null || name.equals(service.getName()) || name.equals(service.getLabel())) { includeService = true; } } if (includeService) { services.add((E) service); } } logger.debug(services.size() + " services of type " + filteredType + "have been found"); return services; } /** * List all logical services filtered by type, and deduplicated. (Read-only) * @param filteredType the class to filter logical services or null to return all services * @return an unmodifiable list of logical services sorted by type and label */ public <E extends LogicalService> Set<E> listLogicalServices(Class<E> filteredType) { return listLogicalServices(filteredType, null); } public boolean isSox() { return sox; } public void setSox(boolean sox) { this.sox = sox; } public void addExecutionNode(ProcessingNode node) { //Note: Assigning the UUID at the time the service is added in the LD. This should //help diagnosing orphan sercvies // node.setName(UUID.randomUUID().toString()); for (ProcessingNode executionNode : this.processingNodes) { Validate.isTrue(!executionNode.getName().equals(node.getName()), "ExecutionNode name expected to be unique, found duplicate:" + name); Validate.isTrue(!executionNode.getLabel().equals(node.getLabel()), "ExecutionNode label expected to be unique, found duplicate:" + label); } node.setLogicalDeployment(this); this.processingNodes.add(node); } public void removeProcessingNode(ProcessingNode jeeProcessing) { // Before removing the execution node, we have to remove all it's // logical service usage (if not, we get a // javax.persistence.EntityNotFoundException: // deleted entity passed to persist) jeeProcessing.removeAllLogicalServiceUsage(jeeProcessing.listLogicalServicesAssociations()); // then we can remove the node this.processingNodes.remove(jeeProcessing); } public void removeProcessingNodes( List<ProcessingNode> processingsList) { while (!processingsList.isEmpty()) { removeProcessingNode(this.processingNodes.get(0)); } } public void removeAllProcessingNodes() { List<ProcessingNode> nodes = this.processingNodes; removeProcessingNodes(nodes); } public void addLogicalService(LogicalService service) { //Note: Assigning the UUID at the time the service is added in the LD. This should //help diagnosing orphan sercvies // service.setName(UUID.randomUUID().toString()); for (LogicalService logicalService : this.logicalServices) { Validate.isTrue(!logicalService.getName().equals(service.getName()), "LogicalService name expected to be unique, found duplicate:" + name); Validate.isTrue(!logicalService.getLabel().equals(service.getLabel()), "LogicalService label expected to be unique, found duplicate:" + label); } service.setLogicalDeployment(this); this.logicalServices.add(service); } /** * removes a Logical Service * @param logicalService * @throws BusinessException */ public void removeLogicalService(LogicalService logicalService) throws BusinessException { // we can remove a logicalService only if it is not associated to a node for (ProcessingNode jeeProcessing : this.listProcessingNodes()) { List<LogicalService> services = jeeProcessing.listLogicalServices(); if (services.contains( logicalService)) { // TODO : create business exception subclass ? where ? throw new BusinessException( "Can't delete a service already associated to a node"); } } //TODO: Delete this block when Matrix service deletion will be refactored bug #96290 int serviceIndex = -1; for (LogicalService service : this.logicalServices) { serviceIndex++; if (service.equals(logicalService, true)) { break; } } if (serviceIndex != -1) { this.logicalServices.remove(serviceIndex); } //TODO: uncomment when Matrix service deletion will be refactored // this.logicalServices.remove(logicalService); } /** * removes all Logical Services * @param logicalServiceList * @throws BusinessException */ public void removeAllLogicalService(List<LogicalService> logicalServiceList) throws BusinessException { while (!logicalServiceList.isEmpty()) { removeLogicalService(this.logicalServices.get(0)); } } /** * Removes all logical services * @throws BusinessException exception thrown when a service is still associated to an execution node */ public void removeAllLogicalService() throws BusinessException { List<LogicalService> services = this.logicalServices; removeAllLogicalService(services); } /** * Utility method to apply consistency checking on the overall model. */ public void checkOverallConsistency() throws LogicalModelNotConsistentException { List<BusinessException> errors = new ArrayList<BusinessException>(); if (processingNodes.isEmpty()) { errors.add(new BusinessException("Please configure at least one JEE processing service which is required for your application")); } for (ProcessingNode executionNode : processingNodes) { try { //Check config service associated to each execNode given do not overlap executionNode.checkConsistency(); } catch (InvalidConfigServiceException e) { errors.add(e); } //Assertions on LogicalDeployment invariants assert executionNode.isAssociationsSymmetryRespected() : "associations between execution nodes and services should be symmetric, check error traces for details"; assert this == executionNode.getLogicalDeployment() : "relation between logical deployment and execution node should be symmetric"; } for (LogicalService service : logicalServices) { assert this == service.getLogicalDeployment() : "relation between logical deployment and service should be symmetrical"; int nbAssociatedExecNodes = service.listLogicalServicesAssociations().size(); if (nbAssociatedExecNodes ==0) { errors.add(new BusinessException("Please make sure each service is associated to at least one processing service (e.g. JeeProcessing). The following service is dangling: " + service.getLabel())); } } // CHECK CONFIG SETS for duplicate keys within each services Set<LogicalConfigService> logicalConfigServices = listLogicalServices(LogicalConfigService.class); for (LogicalConfigService logicalConfigService : logicalConfigServices) { try { LOGICAL_CONFIG_SERVICE_UTILS.parseConfigContent(logicalConfigService.getConfigSetContent()); } catch(InvalidConfigServiceException e) { e.setImpactedElementName(logicalConfigService.getLabel()); errors.add(e); } } Set<LogicalMomService> logicalMomServices = listLogicalServices(LogicalMomService.class); for (LogicalMomService logicalMomService : logicalMomServices) { // Check that the dead letter queue is defined when checked. if (logicalMomService.hasDeadLetterQueue) { if (logicalMomService.getDeadLetterQueueName() == null || logicalMomService.getDeadLetterQueueName().equals("")) { BusinessException be = new BusinessException("Dead letter queue has been enabled but no name has been defined: " + logicalMomService.getDestinationName()); be.setImpactedElementName(logicalMomService.getLabel()); errors.add(be); } } // Check that we don't have duplicated JNDI names. for (LogicalMomService logicalMomService2 : logicalMomServices) { if (logicalMomService != logicalMomService2) { if (logicalMomService.getDestinationName().equals(logicalMomService2.getDestinationName())) { errors.add(new BusinessException("Destination name must be unique. Duplicate destination name: " + logicalMomService.getDestinationName())); } if (logicalMomService.getDestinationName().equals(logicalMomService2.getDeadLetterQueueName())) { errors.add(new BusinessException("Destination name can't be the same as dead letter queue name: " + logicalMomService.getDestinationName())); } } else { if (logicalMomService.getDestinationName().equals(logicalMomService.getDeadLetterQueueName())) { errors.add(new BusinessException("Destination name can't be the same as dead letter queue name: " + logicalMomService.getDestinationName())); } } } } try { //Force validation of constraints expressed in annotations, so that we provide feedback to end-user early on. ValidatorUtil.validate(this); } catch (TechnicalException e) { errors.add(new BusinessException("Invalid architecture: fails javax.validation: " + e, e)); } if (!errors.isEmpty()) { logger.debug("Detected consistency errors in logical model:" + errors); throw new LogicalModelNotConsistentException(errors); } } /** * gets an XML representation of the model * @return */ public String dumpXml() { logger.debug("dumping xml model for "+this + " into a String"); JAXBContext jc; try { jc = JAXBContext.newInstance(LogicalDeployment.class); Marshaller m = jc.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); ByteArrayOutputStream baos=new ByteArrayOutputStream(); m.marshal(this, baos); return baos.toString(); } catch (JAXBException e) { logger.error("Unable to marshall model"); throw new TechnicalException(e); } } public void setTemplate(boolean template) { this.template = template; } public boolean isTemplate() { return template; } @Override protected boolean isFieldExcludedFromToString(String fieldName) { return isFieldExcludedFromToString(fieldName, LogicalDeployment.EXCLUDED_EQUALS_FIELDS); } @Override public boolean equals(Object obj) { //Note: Hibernate implementation of List (PersistentBag) breaks List.equals() contract. //http://docs.jboss.org/hibernate/core/3.2/api/org/hibernate/collection/PersistentBag.html#equals%28java.lang.Object%29 //Therefore, we have to wrap the hibernate collection into a standard one which properly //respect the equals() by comparing elements within it. //In terms of performance impact, this should be quite limited because PersistentBag is backed //by an ArrayList, and therefore this should merely translate into an array copy + one ArrayList //wrapper object creation. if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } LogicalDeployment rhs = (LogicalDeployment) obj; //In addition, we can't simply delegate equals() to super class, otherwise it will pick up the collections //from LogicalDeployment "this" instance using reflection. Therefore we explicitly request to exclude it in //the first equals() call. This will however, take EqualsBuilder equalsBuilder = new EqualsBuilder() .appendSuper(super.equals(obj, EXCLUDED_HIBERNATE_BROKEN_EQUALS_COLLECTIONS)); //Then we manually compare the collections by wrapping them. // We need to sort it first (based on labels + class names) List<ProcessingNode> thisProcessingNodes = new ArrayList<ProcessingNode>(processingNodes); Collections.sort(thisProcessingNodes); List<LogicalService> thisLogicalServices = new ArrayList<LogicalService>(logicalServices); Collections.sort(thisLogicalServices); List<ProcessingNode> rhsProcessingNodes = new ArrayList<ProcessingNode>(rhs.processingNodes); Collections.sort(rhsProcessingNodes); List<LogicalService> rhsLogicalServices = new ArrayList<LogicalService>(rhs.logicalServices); Collections.sort(rhsLogicalServices); boolean equals = equalsBuilder .append(thisProcessingNodes, rhsProcessingNodes) .append(thisLogicalServices, rhsLogicalServices) .isEquals(); return equals; } @Override public int hashCode() { //Note: Same workarounds as for equals //FIXME: may break in NPE when hibernate uses lazy loading of the processingNodes and logicalServices return new HashCodeBuilder(17, 37). appendSuper(super.hashCode(EXCLUDED_HIBERNATE_BROKEN_EQUALS_COLLECTIONS)). append(new ArrayList<ProcessingNode>(processingNodes)). append(new ArrayList<LogicalService>(logicalServices)). toHashCode(); } public LogicalService findLogicalService(String label) { for (LogicalService logicalService : logicalServices) { if (logicalService.getLabel().equals(label)) { return logicalService; } } throw new IllegalArgumentException( "Unknown execution node " + label ); } public boolean noProcessingNodes() { List<JeeProcessing> jeeProcessings = listProcessingNodes(JeeProcessing.class); List<CFJavaProcessing> cfJavaProcessings = listProcessingNodes(CFJavaProcessing.class); return jeeProcessings.isEmpty() && cfJavaProcessings.isEmpty(); } }