| /***************************************************************************** |
| * Copyright (c) 2017 CEA LIST. |
| * |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Ansgar Radermacher ansgar.radermacher@cea.fr |
| * Shuai Li (CEA LIST) <shuai.li@cea.fr> - Bug 530651 |
| * Shuai Li (CEA LIST) <shuai.li@cea.fr> - Bug 531771 |
| * Yoann Farre (CIL4Sys) <yoann.farre@cil4sys.com> - Bug 543072 |
| * |
| *****************************************************************************/ |
| |
| package org.eclipse.papyrus.designer.components.modellibs.core.transformations |
| |
| import org.eclipse.papyrus.designer.transformation.core.transformations.LazyCopier |
| import org.eclipse.uml2.uml.Class |
| import org.eclipse.uml2.uml.Property |
| import org.eclipse.uml2.uml.Port |
| import org.eclipse.papyrus.designer.components.transformation.PortInfo |
| import org.eclipse.papyrus.designer.components.transformation.PortUtils |
| import org.eclipse.uml2.uml.AggregationKind |
| import org.eclipse.uml2.uml.UMLPackage |
| import org.eclipse.uml2.uml.OpaqueBehavior |
| import org.eclipse.uml2.uml.ConnectorEnd |
| import org.eclipse.papyrus.uml.tools.utils.ConnectorUtil |
| import org.eclipse.uml2.uml.Type |
| import java.util.HashMap |
| import java.util.Map |
| import org.eclipse.uml2.uml.Connector |
| import org.eclipse.emf.common.util.EList |
| import org.eclipse.uml2.uml.StructuralFeature |
| import org.eclipse.papyrus.designer.components.transformation.extensions.IOOTrafo |
| import org.eclipse.papyrus.designer.transformation.base.utils.TransformationException |
| import org.eclipse.papyrus.uml.tools.utils.StereotypeUtil |
| import org.eclipse.papyrus.designer.components.FCM.Assembly |
| import org.eclipse.papyrus.designer.transformation.base.utils.CopyUtils |
| import org.eclipse.papyrus.designer.components.transformation.component.PrefixConstants |
| import org.eclipse.papyrus.designer.components.transformation.component.PrefixConstants.CIFvariant |
| import org.eclipse.uml2.uml.MultiplicityElement |
| import org.eclipse.uml2.uml.Element |
| import org.eclipse.papyrus.designer.languages.common.base.ElementUtils |
| import org.eclipse.uml2.uml.Interface |
| import org.eclipse.uml2.uml.BehavioredClassifier |
| import org.eclipse.uml2.uml.Classifier |
| import java.util.List |
| |
| /** |
| * This class realizes the transformation from component-based to object-oriented |
| * models. It includes the replacement of ports and connectors. Ports are |
| * replaced with attributes and access operations, connectors within a composite |
| * by an operation that creates the initial setup. It is an abstract class that is |
| * refined for C++ and Java |
| * |
| * 1. add an operation that allows to retrieve the reference to an interface provided |
| * by a port. This operation has a mapping to a specific name, e.g. get_<port_name> |
| * |
| * 2. add an operation that allows to connect a specific port. |
| * the connect_q operation (*including a storage attribute*) for a port with a required interface |
| * |
| * 3. add an implementation for the getcnx_q operation for a port |
| * with a required interface (the operation itself has been added before) |
| * |
| */ |
| abstract class AbstractCompToOO implements IOOTrafo { |
| |
| protected LazyCopier copier |
| |
| protected String progLang |
| |
| public static final String NL = "\n" |
| |
| override init(LazyCopier copier, Class bootloader) { |
| PrefixConstants.init(CIFvariant.UML); |
| this.copier = copier |
| } |
| |
| /** |
| * de-reference an attribute. Use language specific mechanism, e.g. attributeName-> in case of a C++ pointer |
| */ |
| abstract def String nameRef(Property attribute) |
| |
| /** |
| * apply a stereotypes that transforms an elements into a reference, a pointer in case of C++ |
| */ |
| abstract def void applyRef(Element element) |
| |
| /** |
| * Return the reference of an attribute |
| */ |
| abstract def String getRef(Property attribute) |
| |
| override addPortOperations(Class implementation) { |
| addGetPortOperation(implementation) |
| addConnectPortOperation(implementation) |
| } |
| |
| /** |
| * Add the get_p operation for each port with a provided interface. It also |
| * adds a suitable implementation that evaluates delegation connectors from |
| * the port to a property within the composite. The delegation target could |
| * either be a normal class (no port) or an inner component. |
| * |
| * @param implementation |
| */ |
| def addGetPortOperation(Class implementation) { |
| for (PortInfo portInfo : PortUtils.flattenExtendedPorts(PortUtils.getAllPorts2(implementation))) { |
| val providedIntfs = portInfo.getProvideds() |
| if (providedIntfs !== null && !providedIntfs.empty) { |
| if (providedIntfs.size == 1 && portInfo.port.type instanceof Interface) { |
| addGetPortOperation(implementation, portInfo, providedIntfs.get(0), portInfo.name); |
| } else { |
| for (Interface providedIntf : providedIntfs) { |
| addGetPortOperation(implementation, portInfo, providedIntf, portInfo.name + providedIntf.name); |
| } |
| } |
| } |
| } |
| } |
| |
| def addGetPortOperation(Class implementation, PortInfo portInfo, Interface providedIntf, String portName) { |
| // port provides an interface, add "get_p" operation and implementation |
| val opName = PrefixConstants.getP_Prefix + portName |
| var op = implementation.getOwnedOperation(opName, null, null) |
| if (op !== null) { |
| |
| // operation already exists. Assume that user wants to |
| // override standard delegation |
| if (op.type != providedIntf) { |
| op.createOwnedParameter(Constants.retParamName, providedIntf) |
| } |
| } else { |
| op = implementation.createOwnedOperation(opName, null, null, providedIntf) |
| val retParam = op.getOwnedParameters().get(0) |
| retParam.setName(Constants.retParamName) |
| applyRef(retParam) |
| if (implementation.isAbstract) { |
| // since component is abstract, create abstract delegation operation |
| op.isAbstract = true; |
| } |
| else { |
| addGetPortOperationImpl(implementation, portInfo, providedIntf, portName); |
| } |
| } |
| } |
| |
| def addGetPortOperationImpl(Class implementation, PortInfo portInfo, Interface providedIntf, String portName) { |
| val opName = PrefixConstants.getP_Prefix + portName |
| var op = implementation.getOwnedOperation(opName, null, null) |
| // op must exist (checked by caller) |
| val behavior = implementation.createOwnedBehavior(opName, |
| UMLPackage.eINSTANCE.getOpaqueBehavior()) as OpaqueBehavior |
| op.getMethods().add(behavior) |
| |
| val ces = ConnectorUtil.getDelegations(implementation, portInfo.getModelPort()) |
| |
| // if there is an delegation to an inner property, delegate to it |
| // Make distinction between delegation to component (with a port) or |
| // "normal" class (without). |
| var String body |
| if (!ces.empty) { |
| body = "return " |
| |
| var i = 0 |
| while (i < ces.size && body.equals("return ")) { |
| val part = ces.get(i).partWithPort |
| val role = ces.get(i).role |
| |
| if (role instanceof Port) { |
| val rolePort = role as Port |
| if (rolePort.provideds.contains(providedIntf)) { |
| if (rolePort.provideds.size > 1) { |
| body += '''«part.nameRef»«PrefixConstants.getP_Prefix»«role.name»«providedIntf.name»();''' |
| } else { |
| body += '''«part.nameRef»«PrefixConstants.getP_Prefix»«role.name»();''' |
| } |
| |
| } |
| } else { |
| // role is not a port: connector connects directly to a |
| // structural feature without passing via a port |
| if (role instanceof Property) { |
| val roleType = (role as Property).type; |
| if (roleType instanceof BehavioredClassifier && |
| (roleType as BehavioredClassifier).getInterfaceRealization(null, providedIntf) !== null) { |
| body += role.name |
| } |
| } |
| } |
| i++ |
| } |
| } else { |
| |
| // no delegation, check whether port implements provided interface |
| var implementsIntf = implementation.getInterfaceRealization(null, providedIntf) !== null |
| if (!implementsIntf) { |
| |
| // The extended port itself is not copied to the target |
| // model (since referenced via a stereotype). Therefore, |
| // a port of an extended port still points to the |
| // original model. We try whether the providedIntf |
| // within the target model is within the interface |
| // realizations. |
| val providedIntfInCopy = copier.getCopy(providedIntf) |
| implementsIntf = implementation.getInterfaceRealization(null, providedIntfInCopy) !== null |
| } |
| if (implementsIntf) { |
| body = "return this;" |
| } else { |
| throw new RuntimeException( |
| String.format( |
| "Interface <%s> provided by port <%s> of class <%s> is not implemented by the component itself nor does the port delegate to a part", |
| providedIntf.name, portName, implementation.name)) |
| } |
| } |
| behavior.getLanguages().add(progLang) |
| behavior.getBodies().add(body) |
| } |
| |
| /** |
| * Add a connect_<portName> operation for ports with a required interface. |
| * Whereas operation and a behavior is added for each owned port, a behavior |
| * (method) is needed for ports inherited from a component type (the |
| * behavior is implementation specific, as it needs to take delegation to |
| * parts into account) |
| * |
| * @param implementation |
| */ |
| def addConnectPortOperation(Class implementation) { |
| for (PortInfo portInfo : PortUtils.flattenExtendedPorts(PortUtils.getAllPorts2(implementation))) { |
| val requiredIntfs = portInfo.getRequireds() |
| if (requiredIntfs !== null && !requiredIntfs.empty) { |
| if (requiredIntfs.size == 1 && portInfo.port.type instanceof Interface) { |
| addConnectPortOperation(implementation, portInfo, requiredIntfs.get(0), portInfo.name); |
| } else { |
| for (Interface requiredIntf : requiredIntfs) { |
| addConnectPortOperation(implementation, portInfo, requiredIntf, |
| portInfo.name + requiredIntf.name); |
| } |
| } |
| } |
| } |
| } |
| |
| def addConnectPortOperation(Class implementation, PortInfo portInfo, Interface requiredIntf, String portName) { |
| // port requires an interface, add "connect_p" operation and implementation |
| val opName = PrefixConstants.connectQ_Prefix + portName |
| |
| if (implementation.getOwnedOperation(opName, null, null) !== null) { |
| // do not add the operation, if it already exists. This means that the |
| // user wants to override it with custom behavior. In case of extended |
| // ports, we may have to do that. |
| } else { |
| var op = implementation.createOwnedOperation(opName, null, null) |
| val boolean multiPort = (portInfo.getUpper() > 1) || (portInfo.getUpper() == -1) // -1 indicates "*" |
| val refParam = op.createOwnedParameter("ref", requiredIntf) |
| applyRef(refParam) |
| |
| val behavior = implementation.createOwnedBehavior(opName, |
| UMLPackage.eINSTANCE.getOpaqueBehavior()) as OpaqueBehavior |
| |
| op.getMethods().add(behavior) |
| |
| val ces = ConnectorUtil.getDelegations(implementation, portInfo.getModelPort()) |
| |
| // if there is an delegation to an inner property, delegate to it |
| // Make distinction between delegation to component (with a port) or |
| // "normal" class (without). |
| var String body = "" |
| if (!ces.empty) { |
| var i = 0 |
| while (i < ces.size && body.empty) { |
| val part = ces.get(i).partWithPort |
| |
| val role = ces.get(i).role |
| if (role instanceof Port) { |
| if ((role as Port).requireds.contains(requiredIntf)) { |
| body = part.name |
| |
| // in case of a delegation, use name of target port which might be different |
| var targetOpName = PrefixConstants.connectQ_Prefix + role.name |
| if ((role as Port).requireds.size > 1) { |
| targetOpName += requiredIntf.name |
| } |
| |
| body = '''«part.nameRef»«targetOpName»(ref);''' |
| } |
| } else { |
| if (part.type instanceof Classifier && |
| (part.type as Classifier).getAllUsedInterfaces().contains(requiredIntf)) |
| body += '''«part.name»;''' |
| } |
| i++ |
| } |
| } else { |
| // no delegation - create attribute for port |
| val attributeName = PrefixConstants.attributePrefix + portName |
| var attr = implementation.getOwnedAttribute(attributeName, null) |
| if (attr === null || attr instanceof Port) { |
| attr = implementation.createOwnedAttribute(attributeName, requiredIntf) |
| CopyUtils.copyMultElemModifiers(portInfo.port, attr) |
| |
| // is shared (should store a reference) |
| attr.setAggregation(AggregationKind.SHARED_LITERAL) |
| } |
| body = attributeName |
| if(multiPort) body += "[index]" |
| body += " = ref;" |
| } |
| |
| behavior.getLanguages().add(progLang) |
| behavior.getBodies().add(body) |
| |
| // ------------------------- |
| // add body to get-connection operation (which exists already if the port is also |
| // owned, since it is synchronized automatically during model edit) |
| // getConnQ prefix may be empty to indicate that the port is accessed directly |
| // TODO: reconsider optimization that delegated required ports do not have a |
| // local attribute and associated operation (an inner class may delegate, but the |
| // composite may be using it as well). |
| if ((PrefixConstants.getConnQ_Prefix.length() > 0) && (!ces.empty)) { |
| val getConnOpName = PrefixConstants.getConnQ_Prefix + portName |
| var getConnOp = implementation.getOwnedOperation(getConnOpName, null, null) |
| if (getConnOp === null) { |
| getConnOp = implementation.createOwnedOperation(getConnOpName, null, null, requiredIntf) |
| val retParam = op.getOwnedParameters().get(0) |
| retParam.setName(Constants.retParamName) |
| applyRef(retParam) |
| } |
| val getConnBehavior = implementation.createOwnedBehavior(getConnOpName, |
| UMLPackage.eINSTANCE.getOpaqueBehavior()) as OpaqueBehavior |
| getConnOp.getMethods().add(getConnBehavior) |
| |
| // no delegation |
| val String name = PrefixConstants.attributePrefix + portName |
| body = '''return «name»;''' |
| behavior.getLanguages().add(progLang) |
| behavior.getBodies().add(body) |
| } |
| } |
| } |
| |
| /** |
| * Add an operation "createConnections" that implements the connections |
| * between composite parts. It only takes the assembly connections into |
| * account, since delegation connectors are handled by the get_ and connect_ |
| * port operations above. |
| * |
| * @param implementation |
| */ |
| override addConnectionOperation(Class compositeImplementation) throws TransformationException { |
| var createConnBody = "" |
| val Map<MultiplicityElement, Integer> indexMap = new HashMap<MultiplicityElement, Integer>() |
| |
| for (Connector connector : compositeImplementation.getOwnedConnectors()) { |
| if (ConnectorUtil.isAssembly(connector)) { |
| |
| // Boolean associationBased = false |
| if (connector.ends.size() != 2) { |
| throw new TransformationException( |
| '''Connector <«connector.name»> does not have two ends. This is currently not supported''') |
| } |
| val end1 = connector.ends.get(0) |
| val end2 = connector.ends.get(1) |
| var cmd = '''// realization of connector <«connector.name»>''' + NL |
| if ((end1.role instanceof Port) && PortUtils.isExtendedPort(end1.role as Port)) { |
| val port = end1.role as Port |
| val EList<PortInfo> subPorts = PortUtils.flattenExtendedPort(port) |
| for (PortInfo subPort : subPorts) { |
| cmd += ''' // realization of connection for sub-port «subPort.port.name»''' + NL |
| cmd += connectPorts(connector, end1, end2, subPort.port) |
| cmd += connectPorts(connector, end2, end1, subPort.port) |
| } |
| } else { |
| cmd += connectPorts(connector, end1, end2, null) |
| cmd += connectPorts(connector, end2, end1, null) |
| } |
| createConnBody += cmd + NL |
| } |
| } |
| |
| if (createConnBody.length() > 0) { |
| val operation = compositeImplementation.createOwnedOperation(Constants.CREATE_CONNECTIONS, null, null) |
| |
| val behavior = compositeImplementation.createOwnedBehavior(operation.name, |
| UMLPackage.eINSTANCE.getOpaqueBehavior()) as OpaqueBehavior |
| behavior.getLanguages().add(progLang) |
| behavior.getBodies().add(createConnBody) |
| behavior.setSpecification(operation) |
| } |
| } |
| |
| /** |
| * Create the body code that creates a connection between the two ends |
| * of a connector. This function checks whether the first end really is a receptacle |
| * and the second really is a facet. |
| * |
| * @param indexMap |
| * a map of indices that are used in case of multiplex |
| * receptacles |
| * @param connector |
| * a connector |
| * @param receptacleEnd |
| * an end of the connector that may point to a receptacle port |
| * @param facetEnd |
| * an end of the connector that may point to a facet port |
| * @param subPort |
| * a sub-port in case of extended ports |
| * @return |
| * @throws TransformationException |
| */ |
| def connectPorts(Connector connector, ConnectorEnd receptacleEnd, ConnectorEnd facetEnd, |
| Port subPort) throws TransformationException { |
| val association = connector.type |
| if ((receptacleEnd.role instanceof Port) && (facetEnd.role instanceof Port)) { |
| val facetPort = facetEnd.role as Port |
| val receptaclePort = receptacleEnd.role as Port |
| val facetPI = PortInfo.fromSubPort(facetPort, subPort) |
| val receptaclePI = PortInfo.fromSubPort(receptaclePort, subPort) |
| |
| if ((facetPI.getProvided() !== null) && (receptaclePI.getRequired() !== null)) { |
| val facetPart = facetEnd.partWithPort |
| val receptaclePart = receptacleEnd.partWithPort |
| |
| var subPortName = "" |
| if(subPort !== null) subPortName += "_" + subPort.name |
| |
| var result = "" |
| |
| if (receptaclePI.getRequireds().size > 1) { // receptaclePort requires several interfaces |
| for (requiredInterface : receptaclePI.getRequireds()) { |
| var receptaclePortName = receptaclePI.name + requiredInterface.name |
| var facetPortName = "" |
| |
| if (facetPI.getProvideds().contains(requiredInterface)) { |
| facetPortName += facetPI.name |
| if (facetPI.getProvideds().size > 1) { |
| facetPortName += requiredInterface.name |
| } |
| } |
| |
| if (!facetPortName.empty) { |
| val setter = '''«receptaclePart.nameRef»connect_«receptaclePortName»«subPortName»''' |
| val getter = '''«facetPart.nameRef»get_«facetPortName»«subPortName»()''' |
| result += '''«setter»(«getter»);''' + NL |
| } |
| } |
| } else { // receptaclePort requires only 1 interface |
| var facetPortName = "" |
| |
| if (facetPI.getProvideds().size > 1 && |
| facetPI.getProvideds().contains(receptaclePI.getRequired())) { |
| facetPortName += facetPI.name + receptaclePI.getRequired().name |
| } else if (facetPI.getProvideds().size == 1) { // Original behavior in case of single interface, where we always connect without check of interface consistency |
| facetPortName = facetPI.name |
| } |
| |
| if (!facetPortName.empty) { |
| val setter = '''«receptaclePart.nameRef»connect_«receptaclePort.name»«subPortName»''' |
| val getter = '''«facetPart.nameRef»get_«facetPortName»«subPortName»()''' |
| result += '''«setter»(«getter»);''' + NL |
| } |
| } |
| |
| return result |
| } |
| } else if (receptacleEnd.role instanceof Port) { |
| |
| // only the receptacle end is of type port. |
| val Port receptaclePort = receptacleEnd.role as Port |
| if (PortUtils.getRequired(receptaclePort) !== null) { |
| val facetPart = facetEnd.role as Property |
| val receptaclePart = facetEnd.partWithPort |
| |
| var result = "" |
| |
| if (receptaclePort.getRequireds().size > 1) { // receptaclePort requires several interfaces |
| for (requiredInterface : receptaclePort.getRequireds()) { |
| var receptaclePortName = receptaclePort.name + requiredInterface.name |
| |
| val setter = '''«receptaclePart.nameRef»connect_«receptaclePortName»''' |
| val getter = '''«facetPart.getRef»''' |
| result += '''«setter»(«getter»);''' + NL |
| } |
| } else { |
| val setter = '''«receptaclePart.nameRef»connect_«receptaclePort.name»''' |
| val getter = '''«facetPart.getRef»''' |
| result += '''«setter»(«getter»);''' + NL |
| } |
| |
| return result |
| } |
| } else if (facetEnd.role instanceof Port) { |
| |
| // only the facet end is of type port. Unsupported combination |
| val facetPort = facetEnd.role as Port |
| if (PortUtils.getProvided(facetPort) !== null) { |
| val facetPart = facetEnd.partWithPort |
| val receptaclePart = facetEnd.role as Property |
| |
| var result = "" |
| |
| if (facetPort.getProvideds().size > 1) { // facetPort provides several interfaces |
| for (providedInterface : facetPort.getProvideds()) { |
| var facetPortName = facetPort.name + providedInterface.name |
| |
| val setter = receptaclePart.name |
| val getter = '''«facetPart.nameRef»get_«facetPortName»();''' |
| result += '''«setter» = «getter»;''' + NL |
| } |
| } else { |
| val setter = receptaclePart.name |
| val getter = '''«facetPart.nameRef»get_«facetPort.name»();''' |
| result += '''«setter» = «getter»;''' + NL |
| } |
| |
| return result |
| } |
| } else if (association !== null) { |
| |
| // both connector ends do not target ports. In this case, we require that the connector is typed |
| // with an association. We use this association to find out which end is navigable and assume that |
| // the part pointed to by the other end is a pointer that gets initialized with the part of the |
| // navigable end. |
| val facetPart = facetEnd.role as Property |
| val receptaclePart = receptacleEnd.role as Property |
| |
| val assocProp1 = association.getMemberEnd(null, facetPart.type) |
| |
| if ((assocProp1 !== null) && assocProp1.isNavigable) { |
| var setter = '''«receptaclePart.name».«assocProp1.name»''' |
| val getter = '''«facetPart.name»''' |
| return '''«setter» = «getter»;''' + NL |
| } |
| |
| } else { |
| |
| // not handled (a connector not targeting a port must be typed) |
| throw new TransformationException("Connector <" + connector.name + |
| "> does not use ports, but it is not typed (only connectors between ports should not be typed)") |
| } |
| return "" |
| } |
| |
| /** |
| * Return true, if the bootloader is responsible for the instantiation of a |
| * part. |
| * |
| * If a part is a component type or an abstract implementation, it cannot be |
| * instantiated. Thus, a heir has to be selected in the deployment plan. |
| * Since the selection might be different for different instances of the |
| * composite, the instantiation is not done by the component itself, but by |
| * the bootloader. The bootloader also has to instantiate, if different |
| * allocation variants are required. (this is for instance the case for |
| * distribution connectors and for the system itself) |
| * |
| * If possible, we want to let composites instantiate sub-components, since |
| * this eases the transition to systems which support reconfiguration. |
| * |
| * [TODO: optimization: analyze whether the deployment plan selects a single |
| * implementation. If yes, let the composite instantiate] |
| * |
| * @param implementation a composite component |
| * @return |
| */ |
| static def instantiateViaBootloader(Class implementation) { |
| return implementation.isAbstract() || StereotypeUtil.isApplied(implementation, Assembly) |
| } |
| |
| /** |
| * Return whether a part needs to be instantiated by the bootloader instead |
| * by the composite in which it is contained. The criteria is based on the |
| * question whether the containing composite is flattened, as it is the case |
| * for the system component and the interaction components for distribution. |
| * |
| * @param part |
| * @return |
| */ |
| static def instantiateViaBootloader(StructuralFeature part) { |
| if (part !== null) { |
| if (part.type instanceof Class) { |
| val implementation = part.type as Class |
| // TODO: wrong criteria? (must be shared or not?) |
| return instantiateViaBootloader(implementation) |
| } else { |
| |
| // not a class, assume primitive type instantiated by composite |
| return false |
| } |
| } |
| return false |
| } |
| |
| /** |
| * Returns true if the port delegates to multiple parts/ports. Checks in depth. |
| * @param port |
| * @return port has multiple delegations in depth |
| */ |
| def boolean hasMultipleDelegationsInDepth(Port port) { |
| val owner = port.owner |
| if (owner instanceof Class) { |
| val ces = ConnectorUtil.getDelegations(owner as Class, port) |
| if (ces.size > 1) { |
| return true |
| } |
| if (ces.size == 1) { |
| val ce = ces.get(0) |
| val role = ce.role |
| if (role instanceof Port) { |
| return hasMultipleDelegationsInDepth(role as Port) |
| } |
| } |
| } |
| |
| return false |
| } |
| } |