/*******************************************************************************
* Copyright (c) 2010-2015 Henshin developers. 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:
* TU Berlin, University of Luxembourg, SES S.A.
*******************************************************************************/
package de.tub.tfs.henshin.tggeditor.util.rule.concurrent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.henshin.interpreter.Match;
import org.eclipse.emf.henshin.interpreter.RuleApplication;
import org.eclipse.emf.henshin.interpreter.impl.MatchImpl;
import org.eclipse.emf.henshin.interpreter.impl.RuleApplicationImpl;
import org.eclipse.emf.henshin.model.Attribute;
import org.eclipse.emf.henshin.model.AttributeCondition;
import org.eclipse.emf.henshin.model.Graph;
import org.eclipse.emf.henshin.model.HenshinFactory;
import org.eclipse.emf.henshin.model.Mapping;
import org.eclipse.emf.henshin.model.MappingList;
import org.eclipse.emf.henshin.model.Node;
import org.eclipse.emf.henshin.model.Parameter;
import org.eclipse.emf.henshin.model.Rule;
import org.eclipse.emf.henshin.model.impl.MappingListImpl;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.swt.widgets.Display;
import de.tub.tfs.henshin.tgg.TAttribute;
import de.tub.tfs.henshin.tgg.TGGRule;
import de.tub.tfs.henshin.tgg.TNode;
import de.tub.tfs.henshin.tgg.TripleGraph;
import de.tub.tfs.henshin.tgg.impl.TggFactoryImpl;
import de.tub.tfs.henshin.tgg.interpreter.impl.TggEngineImpl;
import de.tub.tfs.henshin.tgg.interpreter.util.RuleUtil;
import de.tub.tfs.henshin.tggeditor.util.GraphUtil;
import de.tub.tfs.henshin.tggeditor.util.TggHenshinEGraph;
import de.tub.tfs.henshin.tggeditor.util.rule.concurrent.RuleLhsCoSpanList.RuleLhsCoSpan;
import de.tub.tfs.henshin.tggeditor.util.rule.copy.Graph2GraphCopyMappingList;
import de.tub.tfs.muvitor.ui.MuvitorActivator;
/**
*
* @author Jerry
* ConcurrentRuleList generates and stores all possible concurrent rules from a left rule and a right rule
*/
public class ConcurrentRuleList extends LinkedList<TGGRule> {
private static final long serialVersionUID = -6194088318760268591L;
/**
* Inverse of initial left rule that has to be merged with ruleR.
*/
private Rule ruleL; // inverse of initial left rule
/**
* Right rule that is merged with inverse of ruleL.
*/
private Rule ruleR;
/**
* Maps IDs to attribute values that are replaced temporarily by these ids.
*/
private HashMap<String, String> attributeID2Value;
/**
* Constructor of List that contains all possible concurrent rule constructions from ruleLeft and ruleRight.
* @param ruleLeft
* @param ruleRight
*/
public ConcurrentRuleList(final TGGRule ruleLeft, final TGGRule ruleRight){
super();
if (ruleLeft == null || ruleRight == null) {
throw new IllegalArgumentException("Null is not a valid ConcurrentRuleList argument");
}
//copy rules because they are manipulated
//ruleL is inverse of ruleLeft since we have to apply the inverse in order to create LHS of concurrent rule
this.ruleL = ConcurrentRuleUtil.getInverse(ruleLeft);
this.ruleR = ConcurrentRuleUtil.copyRule(ruleRight);
this.attributeID2Value = new HashMap<String, String>();
//copy and remove attribute Conditions
LinkedList<AttributeCondition> attributeConditionsL = new LinkedList<AttributeCondition>();
LinkedList<AttributeCondition> attributeConditionsR = new LinkedList<AttributeCondition>();
attributeConditionsL.addAll(this.ruleL.getAttributeConditions());
attributeConditionsR.addAll(this.ruleR.getAttributeConditions());
this.ruleL.getAttributeConditions().clear();
this.ruleR.getAttributeConditions().clear();
//TODO handle the NAC case
if (this.ruleR.getLhs().getFormula() != null){
System.out.println("rule2 has NAC --> ignore");
return;
}
//generate all possible cospans (intersections) of the LHSides of both ruleL and ruleR
RuleLhsCoSpanList graphsLCoSpans = new RuleLhsCoSpanList(this.ruleL, this.ruleR);
//clear Lhs attributes of rules because the matching target nodes are already fixed,
//and no problems with supposedly matching attributes will occur that way
for (Node nodeLRuleL : this.ruleL.getLhs().getNodes()){
nodeLRuleL.getAttributes().clear();
}
for (Node nodeLRuleR : this.ruleR.getLhs().getNodes()){
nodeLRuleR.getAttributes().clear();
}
//replace attribute values with id values that represent real values
///instead of expressions such as params in order to make rules applicable to the graphs
replaceAttributeValueWithID(this.ruleL.getRhs());
replaceAttributeValueWithID(this.ruleR.getRhs());
int concurrentRuleIndex = 0;
for (RuleLhsCoSpan graphsLCoSpan : graphsLCoSpans){
//copy the generated union C (center graph) of the corresponding cospan twice
//because we have to apply 2 rules to it, ruleL and ruleR
Graph2GraphCopyMappingList graphCoSpanC2graphCoSpanCCopyL = new Graph2GraphCopyMappingList(graphsLCoSpan.getGraphCoSpanC());
Graph2GraphCopyMappingList graphCoSpanC2graphCoSpanCCopyR = new Graph2GraphCopyMappingList(graphsLCoSpan.getGraphCoSpanC());
Graph graphCoSpanCCopyL = graphCoSpanC2graphCoSpanCCopyL.getGraphCopy();
Graph graphCoSpanCCopyR = graphCoSpanC2graphCoSpanCCopyR.getGraphCopy();
//after rule applications, graphCoSpanCCopyL/R will be lhs/rhs graphs of resulting concurrent Rule respectively
graphCoSpanCCopyL.setName("ConcurrentRuleLhs");
graphCoSpanCCopyR.setName("ConcurrentRuleRhs");
//replace attribute values with id values that represent real target graph values
///instead of expressions such as params in order to make rules applicable to the graphs
replaceAttributeValueWithID(graphCoSpanCCopyL);
replaceAttributeValueWithID(graphCoSpanCCopyR);
//avoid multiple matches by match definition
MatchImpl matchL = new MatchImpl(ruleL);
MatchImpl matchR = new MatchImpl(ruleR);
// create EGraphs for rule application and match definition
final TggHenshinEGraph eGraphL = new TggHenshinEGraph(graphCoSpanCCopyL);
final TggHenshinEGraph eGraphR = new TggHenshinEGraph(graphCoSpanCCopyR);
//construct mappings from CoSpanGraphs(L/R) to graphCCopy(L/R) respectively
MappingList graphLRuleL2graphCCopyL = new MappingComposition(
graphsLCoSpan.getGraphCoSpanL2graphCoSpanC(),
graphCoSpanC2graphCoSpanCCopyL, graphCoSpanCCopyL);
MappingList graphLRuleR2graphCCopyL = new MappingComposition(
graphsLCoSpan.getGraphCoSpanR2graphCoSpanC(),
graphCoSpanC2graphCoSpanCCopyL, graphCoSpanCCopyL);
MappingList graphLRuleL2graphCCopyR = new MappingComposition(
graphsLCoSpan.getGraphCoSpanL2graphCoSpanC(),
graphCoSpanC2graphCoSpanCCopyR, graphCoSpanCCopyR);
MappingList graphLRuleR2graphCCopyR = new MappingComposition(
graphsLCoSpan.getGraphCoSpanR2graphCoSpanC(),
graphCoSpanC2graphCoSpanCCopyR, graphCoSpanCCopyR);
for (Node nodeLRuleL : this.ruleL.getLhs().getNodes()) {
Node nodeGraphCCopyL = graphLRuleL2graphCCopyL.getImage(
nodeLRuleL, graphCoSpanCCopyL);
EObject target = eGraphL.getNode2ObjectMap().get(
nodeGraphCCopyL);
matchL.setNodeTarget(nodeLRuleL, target);
}
for (Node nodeLRuleR : this.ruleR.getLhs().getNodes()) {
Node nodeGraphCCopyR = graphLRuleR2graphCCopyR.getImage(
nodeLRuleR, graphCoSpanCCopyR);
EObject target = eGraphR.getNode2ObjectMap().get(
nodeGraphCCopyR);
matchR.setNodeTarget(nodeLRuleR, target);
}
for (Node nodeCoSpanCCopyL : graphCoSpanCCopyL.getNodes()){
Node nodeLRuleL = graphLRuleL2graphCCopyL.getOrigin(nodeCoSpanCCopyL);
//Node nodeLRuleR = graphLRuleR2graphCCopyL.getOrigin(nodeCoSpanCCopyL);
if (nodeLRuleL!=null){
HashSet<Attribute> attributesToRemove = new HashSet<Attribute>();
for (Attribute attributeNodeCoSpanCCopyL : nodeCoSpanCCopyL.getAttributes()){
if (RuleUtil.NEW.equals(((TAttribute)attributeNodeCoSpanCCopyL).getMarkerType())){
attributesToRemove.add(attributeNodeCoSpanCCopyL);
}
}
nodeCoSpanCCopyL.getAttributes().removeAll(attributesToRemove);
}
}
/*
for (Node nodeCoSpanCCopyR : graphCoSpanCCopyR.getNodes()){
Node nodeLRuleL = graphLRuleL2graphCCopyR.getOrigin(nodeCoSpanCCopyR);
Node nodeLRuleR = graphLRuleR2graphCCopyR.getOrigin(nodeCoSpanCCopyR);
if (nodeLRuleL!=null && nodeLRuleR!=null){//if intersecting node
HashSet<Attribute> atrributesNodeCoSpanCCopyRToRemove = new HashSet<Attribute>();
for (Attribute atrributeNodeCoSpanCCopyR : nodeCoSpanCCopyR.getAttributes()){
//if attribute is intersecting remove it
if (AttributeUtil.getAttributeMarker(atrributeNodeCoSpanCCopyR)!=null && AttributeUtil.getAttributeMarker(atrributeNodeCoSpanCCopyR).equals(RuleUtil.INTERSECTING)){
atrributesNodeCoSpanCCopyRToRemove.add(atrributeNodeCoSpanCCopyR);
}
}
nodeCoSpanCCopyR.getAttributes().removeAll(atrributesNodeCoSpanCCopyRToRemove);
}else if (nodeLRuleL==null && nodeLRuleR!=null){//only part of right rule
nodeCoSpanCCopyR.getAttributes().clear();
Test.check(nodeCoSpanCCopyR.getAttributes().isEmpty());
}
}*/
for (Node nodeRRuleL : this.ruleL.getRhs().getNodes()){
for (Attribute attRnodeRRuleL: nodeRRuleL.getAttributes()){
((TAttribute)attRnodeRRuleL).setMarkerType(RuleUtil.NEW);;
}
}
for (Node nodeRRuleR : this.ruleR.getRhs().getNodes()){
for (Attribute attRnodeRRuleR: nodeRRuleR.getAttributes()){
((TAttribute)attRnodeRRuleR).setMarkerType(RuleUtil.NEW);;
}
}
RuleApplication ruleAppL = applyRule(this.ruleL, eGraphL, matchL);
//check whether ruleL was successful, if not then the concurrent rule is not valid
if (ruleAppL!=null && ruleAppL.getResultMatch()!=null){
RuleApplication ruleAppR = applyRule(this.ruleR, eGraphR, matchR);
if (ruleAppR==null || ruleAppR.getResultMatch()==null) continue;
TGGRule concurrentRule = TggFactoryImpl.init().createTGGRule();
//new TripleGraphImpl();
concurrentRule.setName(ConcurrentRuleUtil.getConcurrentRuleName(ruleLeft, ruleRight,concurrentRuleIndex));
//rule applications transformed the union into lhs and rhs of concurrent rule respectively
concurrentRule.setLhs(graphCoSpanCCopyL);
concurrentRule.setRhs(graphCoSpanCCopyR);
((TripleGraph)concurrentRule.getLhs()).setDividerCT_X(((TripleGraph)graphCoSpanCCopyL).getDividerCT_X());
((TripleGraph)concurrentRule.getLhs()).setDividerSC_X(((TripleGraph)graphCoSpanCCopyL).getDividerSC_X());
((TripleGraph)concurrentRule.getLhs()).setDividerMaxY(((TripleGraph)graphCoSpanCCopyL).getDividerMaxY());
((TripleGraph)concurrentRule.getLhs()).setDividerYOffset(((TripleGraph)graphCoSpanCCopyL).getDividerYOffset());
((TripleGraph)concurrentRule.getRhs()).setDividerCT_X(((TripleGraph)graphCoSpanCCopyR).getDividerCT_X());
((TripleGraph)concurrentRule.getRhs()).setDividerSC_X(((TripleGraph)graphCoSpanCCopyR).getDividerSC_X());
((TripleGraph)concurrentRule.getRhs()).setDividerMaxY(((TripleGraph)graphCoSpanCCopyR).getDividerMaxY());
((TripleGraph)concurrentRule.getRhs()).setDividerYOffset(((TripleGraph)graphCoSpanCCopyR).getDividerYOffset());
for (Node nodeRRuleL : this.ruleL.getRhs().getNodes()){
TNode concurrentNodeL = getAppResNodeFromRuleNode(ruleAppL, nodeRRuleL);
//if (concurrentNodeL.getX()!=0 && concurrentNodeL.getX()!=((TNode)nodeRRuleL).getX()) throw new IllegalArgumentException("remove this");
concurrentNodeL.setX(((TNode)nodeRRuleL).getX());
concurrentNodeL.setY(((TNode)nodeRRuleL).getY());
}
for (Node nodeRRuleR : this.ruleR.getRhs().getNodes()){
TNode concurrentNodeR = getAppResNodeFromRuleNode(ruleAppR, nodeRRuleR);
//if (concurrentNodeR.getX()!=0) {System.out.println("x was "+concurrentNodeR.getX()+ " and is "+((TNode)nodeRRuleR).getX());};
concurrentNodeR.setX(((TNode)nodeRRuleR).getX());
concurrentNodeR.setY(((TNode)nodeRRuleR).getY());
}
MappingList graphCoSpanCCopyL2graphCoSpanC = ConcurrentRuleUtil.getInverseMappingList(graphCoSpanC2graphCoSpanCCopyL);
MappingList unionL2unionR = new MappingComposition(graphCoSpanCCopyL2graphCoSpanC, graphCoSpanC2graphCoSpanCCopyR, concurrentRule.getRhs());
MappingList concurrentLHS2unionR = restrictToDomain(unionL2unionR, concurrentRule.getLhs());
concurrentRule.getMappings().addAll(concurrentLHS2unionR);
// for (Mapping m : ruleLhsCoSpan.getGraphRRuleL2GraphRRuleR()){
// Node origin = m.getOrigin();
// Node image = m.getImage();
// TNode nodeLConcurrentRule = getAppResNodeFromRuleNode(ruleAppL, origin);
// TNode nodeRConcurrentRule = getAppResNodeFromRuleNode(ruleAppR, image);
// Test.out("ADD Mapping 0:"+Test.outNodeRepresentation(nodeLConcurrentRule)+"-->"+Test.outNodeRepresentation(nodeRConcurrentRule));
// concurrentRule.getMappings().add(nodeLConcurrentRule, nodeRConcurrentRule);
// }
//
// Test.check(!graphCoSpanC2graphCoSpanCCopyR.isEmpty());
//
// // mappings
// for (Node nodeRRuleL : this.ruleL.getRhs().getNodes()){
//
// TNode nodeLConcurrentRule = getAppResNodeFromRuleNode(ruleAppL, nodeRRuleL);
// Test.check(nodeLConcurrentRule!=null);
// if (concurrentRule.getMappings().getImage(nodeLConcurrentRule, concurrentRule.getRhs())==null){
// Node nodeGraphCoSpanC = ruleLhsCoSpan.getGraphRRuleL2GraphCoSpanC().getImage(nodeRRuleL, ruleLhsCoSpan.getGraphCoSpanC());
// Test.check(nodeGraphCoSpanC!=null);
// //Node nodeRRuleR = ruleLhsCoSpan.getGraphCoSpanC2graphRRuleR().getImage(nodeGraphCoSpanC, ruleR.getRhs());
// Node nodeRConcurrentRule = graphCoSpanC2graphCoSpanCCopyR.getImage(nodeGraphCoSpanC);
// Test.check(nodeRConcurrentRule!=null);
// Test.out("ADD Mapping 2:"+Test.outNodeRepresentation(nodeLConcurrentRule)+"-->"+Test.outNodeRepresentation(nodeRConcurrentRule));
// concurrentRule.getMappings().add(nodeLConcurrentRule, nodeRConcurrentRule);
// }
// }
//
//
//
// Test.check(!graphCoSpanC2graphCoSpanCCopyL.isEmpty());
// for (Node nodeLRuleR : this.ruleR.getLhs().getNodes()){
// TNode nodeRConcurrentRule = getAppResNodeFromRuleNode(ruleAppR, ruleR.getMappings().getImage(nodeLRuleR, ruleR.getRhs()));
// Test.check(nodeRConcurrentRule!=null);
// if (concurrentRule.getMappings().getOrigin(nodeRConcurrentRule)==null){
// Node nodeRGraphC = ruleLhsCoSpan.getGraphCoSpanR2graphCoSpanC().getImage(nodeLRuleR, ruleLhsCoSpan.getGraphCoSpanC());
// Test.check(nodeRGraphC!=null);
// Node nodeLConcurrentRule = graphCoSpanC2graphCoSpanCCopyL.getImage(nodeRGraphC);
// if (nodeLConcurrentRule!=null){
// Test.out("ADD Mapping 3:"+Test.outNodeRepresentation(nodeLConcurrentRule)+"-->"+Test.outNodeRepresentation(nodeRConcurrentRule));
// concurrentRule.getMappings().add(nodeLConcurrentRule, nodeRConcurrentRule);
// }
// }
// }
/**
for (Node concurrentRuleNodeL : concurrentRule.getLhs().getNodes()){
if (concurrentRule.getAllMappings().getImage(concurrentRuleNodeL, concurrentRule.getRhs())==null){
Node ruleRNodeR = ruleCospan.getGraphC2graphRRuleR().getImage(graphCCopyL.getOrigin(concurrentRuleNodeL), ruleCospan.getRuleR().getRhs());
TNode concurrentRuleNodeR = getAppNodeFromRuleNode(ruleRNodeR, ruleAppR);
//TNode concurrentRuleNodeL = getAppNodeFromRuleNode(nLhs, ruleAppL);
System.out.println("r l "+concurrentRuleNodeR);
System.out.println("r l "+concurrentRuleNodeL);
concurrentRule.getAllMappings().add(concurrentRuleNodeL, concurrentRuleNodeR);
System.out.println("Good news");
}else{
System.out.println("good news: mapping is consistent");
}
}**/
concurrentRule.getAttributeConditions().addAll(attributeConditionsR);
concurrentRule.getAttributeConditions().addAll(attributeConditionsL);
Test.check(
concurrentRule.getAttributeConditions().isEmpty()==
(attributeConditionsR.isEmpty() && attributeConditionsL.isEmpty()));
AttributeCondition ac = graphsLCoSpan.getIntersectionHandler().getAttributeCondition();
if (!ac.getConditionText().equals("")){
ac.setRule(concurrentRule);
concurrentRule.getAttributeConditions().add(ac);
}
//add params
boolean isEmpty = ruleR.getParameters().isEmpty();
LinkedList<Parameter> paramsR = new LinkedList<Parameter>(ruleR.getParameters());
LinkedList<Parameter> paramsL = new LinkedList<Parameter>(ruleL.getParameters());
if (!isEmpty){
Test.check(!ruleR.getParameters().isEmpty());
}
for (Parameter paramR : paramsR){
for (Parameter paramL : paramsL){
if (paramR.getName().equals(paramL.getName())){
concurrentRule.setName("W_"+concurrentRule.getName());
break;
}
}
}
Iterator<Parameter> paramRIterator = paramsR.iterator();
while(paramRIterator.hasNext()){
Parameter param = paramRIterator.next();
Parameter parameter = HenshinFactory.eINSTANCE.createParameter();
parameter.setName(param.getName());
concurrentRule.getParameters().add(parameter);
}
Iterator<Parameter> paramLIterator = paramsL.iterator();
while(paramLIterator.hasNext()){
Parameter param = paramLIterator.next();
Parameter parameter = HenshinFactory.eINSTANCE.createParameter();
parameter.setName(param.getName());
concurrentRule.getParameters().add(parameter);
}
if (!isEmpty){
Test.check(!ruleR.getParameters().isEmpty());
}
GraphUtil.removeDoubleEdges(concurrentRule.getLhs());
GraphUtil.removeDoubleEdges(concurrentRule.getRhs());
graphsLCoSpan.getIntersectionHandler().replaceAttributeHashValuesWithInitialExpressionsAndSetMarkers(concurrentRule, attributeID2Value);
ConcurrentRuleUtil.mark(concurrentRule);
this.add(concurrentRule);
concurrentRuleIndex++;
}
}
if (ConcurrentRuleUtil.isConcurrent(ruleLeft, ruleRight) && this.isEmpty()) ;
//throw new IllegalArgumentException("Generation of Concurrent Rules Failed because of special case: "+ruleLeft.getName()+" merging with "+ruleRight.getName());
}
/*Begin static helper functions section*/
/*End static helper functions section*/
private void replaceAttributeValueWithID(Graph graph){
for (Node node : graph.getNodes()){
for (Attribute attribute : node.getAttributes()){
if (attribute.getValue()==null) return;
int h = attribute.getValue().hashCode();
String hash = "\""+h+"\"";
while (this.attributeID2Value.containsKey(hash)){
if (!this.attributeID2Value.get(hash).equals(attribute.getValue())){
h += 1;
//hash = Double.toString(h);
hash = "\""+h+"\"";
}else{
break;
};
}
this.attributeID2Value.put(hash, attribute.getValue());
attribute.setValue(hash);
}
}
}
private MappingList restrictToDomain(MappingList mappingL, Graph lhs) {
// TODO Auto-generated method stub
MappingList restrictedMappingList=new MappingListImpl();
for(Mapping m: mappingL){
if(lhs.getNodes().contains(m.getOrigin()))
restrictedMappingList.add(m);
}
return restrictedMappingList;
}
private static TNode getAppResNodeFromRuleNode(RuleApplication ruleApp, Node ruleNode){
Match match = ruleApp.getResultMatch();
TggHenshinEGraph henshinEGraph = (TggHenshinEGraph)(ruleApp.getEGraph());
EObject ruleNodeTarget = match.getNodeTarget(ruleNode); //get resulting nodeObject of rule applicatoin
return (TNode)henshinEGraph.getObject2NodeMap().get(ruleNodeTarget);
}
private static RuleApplication applyRule(Rule rule, TggHenshinEGraph henshinGraph, Match match){
if (match==null) match = new MatchImpl(rule);
//if (match==null || match.getRule()!=rule) throw new IllegalArgumentException("match has to correspond to rule");
//final TggHenshinEGraph henshinGraph = new TggHenshinEGraph(graph);
/*
Map<Node,EObject> node2eObject;
Map<EObject, Node> eObject2Node;
eObject2Node = henshinGraph.getObject2NodeMap();
node2eObject = henshinGraph.getNode2ObjectMap();*/
TggEngineImpl emfEngine = new TggEngineImpl(henshinGraph);
RuleApplicationImpl ruleApplication = new RuleApplicationImpl(emfEngine);
ruleApplication.setRule(rule);
ruleApplication.setEGraph(henshinGraph);
try {
/*
Iterator<Match> matchesIterator = emfEngine
.findMatches(rule, henshinGraph,
match//new MatchImpl(rule)
).iterator();
if (!matchesIterator.hasNext()) return null;*/
ruleApplication.setPartialMatch(match);
boolean appliceable = ruleApplication.execute(null);
if (!appliceable) return null;
return ruleApplication;
} catch (Exception ex) {
ex.printStackTrace();
ErrorDialog.openError(
Display.getDefault().getActiveShell(),
"Execute Failure",
"The rule [" + ruleApplication.getRule().getName()
+ "] couldn't be applied.",
new Status(IStatus.ERROR, MuvitorActivator.PLUGIN_ID, ex
.getMessage(), ex.getCause()));
return null;
}
}
}