/* $Id: TransitionNotationUml.java 18852 2010-11-20 19:27:11Z mvw $ ***************************************************************************** * Copyright (c) 2009-2010 Contributors - see below * 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: * Michiel van der Wulp ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 2005-2009 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.notation.providers.uml; import java.text.ParseException; import java.util.Collection; import java.util.Iterator; import java.util.StringTokenizer; import org.argouml.application.events.ArgoEventPump; import org.argouml.application.events.ArgoEventTypes; import org.argouml.application.events.ArgoHelpEvent; import org.argouml.i18n.Translator; import org.argouml.model.Model; import org.argouml.model.StateMachinesFactory; import org.argouml.notation.NotationSettings; import org.argouml.notation.providers.TransitionNotation; /** * UML Notation for the text shown next to a Transition. * * @author Michiel van der Wulp */ public class TransitionNotationUml extends TransitionNotation { /** * The constructor. * * @param transition the transition represented by this notation */ public TransitionNotationUml(Object transition) { super(transition); } /* * @see org.argouml.uml.notation.NotationProvider#parse(java.lang.Object, java.lang.String) */ public void parse(Object modelElement, String text) { try { parseTransition(modelElement, text); } catch (ParseException pe) { String msg = "statusmsg.bar.error.parsing.transition"; Object[] args = { pe.getLocalizedMessage(), Integer.valueOf(pe.getErrorOffset()), }; ArgoEventPump.fireEvent(new ArgoHelpEvent( ArgoEventTypes.HELP_CHANGED, this, Translator.messageFormat(msg, args))); } } /** * Parse a transition description line of the form:<pre> * "event-signature [guard-condition] / action-expression". * </pre> * * A ";" is not interpreted as having any special meaning. <p> * * The "event-signature" may be one of the 4 * formats:<ul> * <li> ChangeEvent: "when(condition)" * <li> TimeEvent: "after(duration)" * <li> CallEvent: "a(parameter-list)". * <li> SignalEvent: any string without (). * </ul> * * Remark: The UML standard does not make a distinction between * the syntax of a CallEvent and SignalEvent: * both may have parameters between (). * For simplicity and user-friendliness, we chose for this distinction. * If a user wants parameters for a SignalEvent, * then he may add them in the properties panels, but not on the diagram. * <p> * * An alternative solution would be to create a CallEvent by default, * and when editing an existing event, do not change the type.<p> * * TODO: This function fails when the event-signature contains a "[" * or a "/". See issue 5983 for other cases that were * a problem in the past. * * @param trans the transition object to which this string applies * @param s the string to be parsed * @return the transition object * @throws ParseException when no matching [] are found */ protected Object parseTransition(Object trans, String s) throws ParseException { s = s.trim(); int a = s.indexOf("["); int b = s.indexOf("]"); int c = s.indexOf("/"); if (((a < 0) && (b >= 0)) || ((b < 0) && (a >= 0)) || (b < a)) { String msg = "parsing.error.transition.no-matching-square-brackets"; throw new ParseException(Translator.localize(msg), 0); } if ((c >= 0) && (c < b) && (c > a) && (a > 0)) { String msg = "parsing.error.transition.found-bracket-instead-slash"; throw new ParseException(Translator.localize(msg), 0); } String[] s1 = s.trim().split("/", 2); String eg = s1[0].trim(); String[] s2 = eg.split("\\[", 2); if (s2[0].trim().length() > 0) { parseTrigger(trans, s2[0].trim()); } if (s2.length > 1) { if (s2[1].trim().endsWith("]")) { String g = s2[1].trim(); g = g.substring(0, g.length() - 1).trim(); if (g.length() > 0) { parseGuard(trans, g); } } } if (s1.length > 1) { if (s1[1].trim().length() > 0) { parseEffect(trans, s1[1].trim()); } } return trans; } /** * Parse the Event that is the trigger of the given transition. * * @param trans the transition which is triggered by the given event * @param trigger the given trigger * @throws ParseException */ private void parseTrigger(Object trans, String trigger) throws ParseException { // let's look for a TimeEvent, ChangeEvent, CallEvent or SignalEvent String s = ""; boolean timeEvent = false; boolean changeEvent = false; boolean callEvent = false; boolean signalEvent = false; trigger = trigger.trim(); StringTokenizer tokenizer = new StringTokenizer(trigger, "()"); String name = tokenizer.nextToken().trim(); if (name.equalsIgnoreCase("after")) { timeEvent = true; } else if (name.equalsIgnoreCase("when")) { changeEvent = true; } else { // the part after the || is for when there's nothing between the () if (tokenizer.hasMoreTokens() || (trigger.indexOf("(") > 0) || (trigger.indexOf(")") > 1)) { callEvent = true; if (!trigger.endsWith(")") || !(trigger.indexOf("(") > 0)) { String msg = "parsing.error.transition.no-matching-brackets"; throw new ParseException( Translator.localize(msg), 0); } } else { signalEvent = true; } } if (timeEvent || changeEvent || callEvent) { if (tokenizer.hasMoreTokens()) { s = tokenizer.nextToken().trim(); } // else the empty s will do } /* * We can distinguish between 4 cases: * 1. A trigger is given. None exists yet. * 2. The trigger was present, and it is the same type, * or a different type, and its text is changed, or the same. * 3. A trigger is not given. None exists yet. * 4. The name of the trigger was present, but is removed. * The reaction in these cases should be: * 1. Find the referred trigger (issue 5988) or create a new one, and hook it to the transition. * 2. Rename the trigger. * 3. Nop. * 4. Unhook and erase the existing trigger. */ Object evt = Model.getFacade().getTrigger(trans); /* It is safe to give a null to the next function, * since a statemachine is always composed by a model anyhow. */ Object ns = Model.getStateMachinesHelper() .findNamespaceForEvent(trans, null); StateMachinesFactory sMFactory = Model.getStateMachinesFactory(); boolean weHaveAnEvent = false; if (trigger.length() > 0) { // case 1 and 2 if (evt == null) { // case 1 if (timeEvent) { // after(...) evt = findOrBuildTimeEvent(s, ns); /* Do not set the name. */ } if (changeEvent) { // when(...) evt = findOrBuildChangeEvent(s, ns); /* Do not set the name. */ } if (callEvent) { // operation(paramlist) String triggerName = trigger.indexOf("(") > 0 ? trigger.substring(0, trigger.indexOf("(")).trim() : trigger; /* This case is a bit different, because of the parameters. * If the event already exists, the parameters are ignored. */ evt = findCallEvent(triggerName, ns); if (evt == null) { evt = sMFactory.buildCallEvent(trans, triggerName, ns); // and parse the parameter list NotationUtilityUml.parseParamList(evt, s, 0); } } if (signalEvent) { // signalname evt = findOrBuildSignalEvent(trigger, ns); } weHaveAnEvent = true; } else { // case 2 if (timeEvent) { if (Model.getFacade().isATimeEvent(evt)) { /* Just change the time expression */ Object timeExpr = Model.getFacade().getWhen(evt); if (timeExpr == null) { // we have an event without expression timeExpr = Model.getDataTypesFactory().createTimeExpression("", s); Model.getStateMachinesHelper().setWhen(evt, timeExpr); } else { Model.getDataTypesHelper().setBody(timeExpr, s); } } else { /* It's a time-event now, * but was of another type before! */ delete(evt); /* TODO: What if used elsewhere? */ evt = sMFactory.buildTimeEvent(s, ns); weHaveAnEvent = true; } } if (changeEvent) { if (Model.getFacade().isAChangeEvent(evt)) { /* Just change the ChangeExpression */ Object changeExpr = Model.getFacade().getChangeExpression(evt); if (changeExpr == null) { /* Create a new expression: */ changeExpr = Model.getDataTypesFactory() .createBooleanExpression("", s); Model.getStateMachinesHelper().setExpression(evt, changeExpr); } else { Model.getDataTypesHelper().setBody(changeExpr, s); } } else { /* The parsed text describes a change-event, * but the model contains another type! */ delete(evt); /* TODO: What if used elsewhere? */ evt = sMFactory.buildChangeEvent(s, ns); weHaveAnEvent = true; } } if (callEvent) { if (Model.getFacade().isACallEvent(evt)) { /* Just change the Name and linked operation */ String triggerName = trigger.indexOf("(") > 0 ? trigger.substring(0, trigger.indexOf("(")).trim() : trigger; if (!Model.getFacade().getName(evt) .equals(triggerName)) { Model.getCoreHelper().setName(evt, triggerName); } /* TODO: Change the linked operation. */ } else { delete(evt); /* TODO: What if used elsewhere? */ evt = sMFactory.buildCallEvent(trans, trigger, ns); // and parse the parameter list NotationUtilityUml.parseParamList(evt, s, 0); weHaveAnEvent = true; } } if (signalEvent) { if (Model.getFacade().isASignalEvent(evt)) { /* Just change the Name and linked signal */ if (!Model.getFacade().getName(evt).equals(trigger)) { Model.getCoreHelper().setName(evt, trigger); } /* TODO: link to the Signal. */ } else { delete(evt); /* TODO: What if used elsewhere? */ evt = sMFactory.buildSignalEvent(trigger, ns); weHaveAnEvent = true; } } } if (weHaveAnEvent && (evt != null)) { Model.getStateMachinesHelper().setEventAsTrigger(trans, evt); } } else { // case 3 and 4 if (evt == null) { /* case 3 */ } else { // case 4 delete(evt); // erase it } } } protected Object findOrBuildSignalEvent(String trigger, Object ns) { StateMachinesFactory sMFactory = Model.getStateMachinesFactory(); if ((trigger == null) || ("".equals(trigger.trim()))) { return sMFactory.buildSignalEvent(trigger, ns); } Object result = null; Object type = Model.getMetaTypes().getSignalEvent(); Collection events = Model.getModelManagementHelper() .getAllModelElementsOfKind(ns, type); for (Object event : events) { if (trigger.equals(Model.getFacade().getName(event))) { result = event; break; } } if (result == null) { result = sMFactory.buildSignalEvent(trigger, ns); } return result; } protected Object findOrBuildTimeEvent(String timeexpr, Object ns) { StateMachinesFactory sMFactory = Model.getStateMachinesFactory(); if ((timeexpr == null) || ("".equals(timeexpr.trim()))) { return sMFactory.buildTimeEvent(timeexpr, ns); } Object result = null; Object type = Model.getMetaTypes().getTimeEvent(); Collection events = Model.getModelManagementHelper() .getAllModelElementsOfKind(ns, type); for (Object event : events) { Object expression = Model.getFacade().getExpression(event); if (expression != null) { if (timeexpr.equals(Model.getFacade().getBody(expression))) { result = event; break; } } } if (result == null) { result = sMFactory.buildTimeEvent(timeexpr, ns); } return result; } protected Object findOrBuildChangeEvent(String changeexpr, Object ns) { StateMachinesFactory sMFactory = Model.getStateMachinesFactory(); if ((changeexpr == null) || ("".equals(changeexpr.trim()))) { return sMFactory.buildChangeEvent(changeexpr, ns); } Object result = null; Object type = Model.getMetaTypes().getChangeEvent(); Collection events = Model.getModelManagementHelper() .getAllModelElementsOfKind(ns, type); for (Object event : events) { Object expression = Model.getFacade().getExpression(event); if (expression != null) { if (changeexpr.equals(Model.getFacade().getBody(expression))) { result = event; break; } } } if (result == null) { result = sMFactory.buildChangeEvent(changeexpr, ns); } return result; } protected Object findCallEvent(String callexpr, Object ns) { if ((callexpr == null) || ("".equals(callexpr.trim()))) { return null; } Object result = null; Object type = Model.getMetaTypes().getCallEvent(); Collection events = Model.getModelManagementHelper() .getAllModelElementsOfKind(ns, type); for (Object event : events) { if (callexpr.equals(Model.getFacade().getName(event))) { /* Do not check if the parameters match. */ result = event; break; } } return result; } /** * Handle the Guard of a Transition.<p> * * We can distinct between 4 cases:<ol> * <li>A guard is given. None exists yet. * <li>The expression of the guard was present, but is altered. * <li>A guard is not given. None exists yet. * <li>The expression of the guard was present, but is removed. * </ol> * * The reaction in these cases should be:<ol> * <li>Create a new guard, set its name, language & expression, * and hook it to the transition. * <li>Change the guard's expression. Leave the name & language * untouched. See also issue 2742. * <li>Nop. * <li>Unhook and erase the existing guard. * </ol> * * @param trans the UML element transition * @param guard the string that represents the guard expression */ private void parseGuard(Object trans, String guard) { Object g = Model.getFacade().getGuard(trans); if (guard.length() > 0) { if (g == null) { // case 1 /*TODO: In the next line, I should use buildGuard(), * but it doesn't show the guard on the diagram... * Why? (MVW) */ g = Model.getStateMachinesFactory().createGuard(); if (g != null) { Model.getStateMachinesHelper().setExpression(g, Model.getDataTypesFactory() .createBooleanExpression("", guard)); Model.getCoreHelper().setName(g, "anon"); Model.getCommonBehaviorHelper().setTransition(g, trans); // NSUML does this (?) // Model.getFacade().setGuard(trans, g); } } else { // case 2 Object expr = Model.getFacade().getExpression(g); String language = ""; /* TODO: This does not work! (MVW) Model.getFacade().setBody(expr,guard); Model.getFacade().setExpression(g,expr); */ //hence a less elegant workaround that works: if (expr != null) { language = Model.getDataTypesHelper().getLanguage(expr); } Model.getStateMachinesHelper().setExpression(g, Model.getDataTypesFactory() .createBooleanExpression(language, guard)); /* TODO: In this case, the properties panel is not updated with the changed expression! */ } } else { if (g == null) { /* case 3 */ } else { // case 4 delete(g); // erase it } } } /** * Handle the Effect (Action) of a Transition.<p> * * We can distinct between 4 cases:<ul> * <li>1. An effect is given. None exists yet. * <li>2. The expression of the effect was present, but is altered. * <li>3. An effect is not given. None exists yet. * <li>4. The expression of the effect was present, but is removed. * </ul> * * The reaction in these cases should be:<ul> * <li>1. Create a new CallAction, set its name, language & * expression, and hook it to the transition. * <li>2. Change the effect's expression. Leave the actiontype, name * & language untouched. * <li>3. Nop. * <li>4. Unhook and erase the existing effect. * </ul> * * @param actions the string to be parsed * @param trans the transition that causes the effect (actions) */ private void parseEffect(Object trans, String actions) { Object effect = Model.getFacade().getEffect(trans); if (actions.length() > 0) { if (effect == null) { // case 1 effect = Model.getCommonBehaviorFactory() .createCallAction(); /* And hook it to the transition immediately, * so that an exception can not cause it to remain dangling: */ Model.getStateMachinesHelper().setEffect(trans, effect); Model.getCommonBehaviorHelper().setScript(effect, Model.getDataTypesFactory() .createActionExpression(""/*language*/, actions)); Model.getCoreHelper().setName(effect, "anon"); } else { // case 2 Object script = Model.getFacade().getScript(effect); String language = (script == null) ? null : Model.getDataTypesHelper().getLanguage(script); Model.getCommonBehaviorHelper().setScript(effect, Model.getDataTypesFactory() .createActionExpression(language, actions)); } } else { // case 3 & 4 if (effect == null) { // case 3 } else { // case 4 delete(effect); // erase it } } } /** * This deletes modelelements, and swallows null without barking. * * @author Michiel van der Wulp * @param obj * the modelelement to be deleted */ private void delete(Object obj) { if (obj != null) { Model.getUmlFactory().delete(obj); } } /* * @see org.argouml.uml.notation.NotationProvider#getParsingHelp() */ public String getParsingHelp() { return "parsing.help.fig-transition"; } @Override public String toString(Object modelElement, NotationSettings settings) { return toString(modelElement); } private String toString(Object modelElement) { Object trigger = Model.getFacade().getTrigger(modelElement); Object guard = Model.getFacade().getGuard(modelElement); Object effect = Model.getFacade().getEffect(modelElement); String t = generateEvent(trigger); String g = generateGuard(guard); String e = NotationUtilityUml.generateActionSequence(effect); if (g.length() > 0) { t += " [" + g + "]"; } if (e.length() > 0) { t += " / " + e; } return t; } /** * Generates the text for a (trigger) event. * * @param m Object of any MEvent kind * @return the string representing the event */ private String generateEvent(Object m) { if (m == null) { return ""; } StringBuffer event = new StringBuffer(); if (Model.getFacade().isAChangeEvent(m)) { event.append("when("); event.append( generateExpression(Model.getFacade().getExpression(m))); event.append(")"); } else if (Model.getFacade().isATimeEvent(m)) { event.append("after("); event.append( generateExpression(Model.getFacade().getExpression(m))); event.append(")"); } else if (Model.getFacade().isASignalEvent(m)) { event.append(Model.getFacade().getName(m)); } else if (Model.getFacade().isACallEvent(m)) { event.append(Model.getFacade().getName(m)); event.append(generateParameterList(m)); } return event.toString(); } /** * Generates a string representing the given Guard. <p> * * If there is an expression, then its body text is returned. * Else, a 0 length string is returned. <p> * * Apparently, the AndroMDA people are convinced that the * name of the guard should be shown on the diagram, while this * is not correct according the UML standard. * ArgoUML does not support this feature because of its down-side: * The name would end up in the expression * if you edit the transition text on the diagram, * while the name remains containing the old value. * * @param m the UML Guard object * @return a string */ private String generateGuard(Object m) { if (m != null) { if (Model.getFacade().getExpression(m) != null) { return generateExpression(Model.getFacade().getExpression(m)); } } return ""; } /** * Generates a list of parameters. The parameters belong to the * given object. The returned string will have the following * syntax:<p> * * (param1, param2, param3, ..., paramN)<p> * * If there are no parameters, then "()" is returned. * * @param parameterListOwner the 'owner' of the parameters * @return the generated parameter list */ private String generateParameterList(Object parameterListOwner) { Iterator it = Model.getFacade().getParameters(parameterListOwner).iterator(); StringBuffer list = new StringBuffer(); list.append("("); if (it.hasNext()) { while (it.hasNext()) { Object param = it.next(); list.append(generateParameter(param)); if (it.hasNext()) { list.append(", "); } } } list.append(")"); return list.toString(); } private String generateExpression(Object expr) { if (Model.getFacade().isAExpression(expr)) { Object body = Model.getFacade().getBody(expr); if (body != null) { return (String) body; } } return ""; } /** * Generates the representation of a parameter on the display * (diagram). The string to be returned will have the following * syntax:<p> * * kind name : type-expression = default-value * * @param parameter the parameter * @return the generated text */ public String generateParameter(Object parameter) { StringBuffer s = new StringBuffer(); s.append(generateKind(Model.getFacade().getKind(parameter))); if (s.length() > 0) { s.append(" "); } s.append(Model.getFacade().getName(parameter)); String classRef = generateClassifierRef(Model.getFacade().getType(parameter)); if (classRef.length() > 0) { s.append(" : "); s.append(classRef); } String defaultValue = generateExpression(Model.getFacade().getDefaultValue(parameter)); if (defaultValue.length() > 0) { s.append(" = "); s.append(defaultValue); } return s.toString(); } private String generateKind(Object /*Parameter etc.*/ kind) { StringBuffer s = new StringBuffer(); if (kind == null /* "in" is the default */ || kind == Model.getDirectionKind().getInParameter()) { s.append(/*"in"*/ ""); /* See issue 3421. */ } else if (kind == Model.getDirectionKind().getInOutParameter()) { s.append("inout"); } else if (kind == Model.getDirectionKind().getReturnParameter()) { // return nothing } else if (kind == Model.getDirectionKind().getOutParameter()) { s.append("out"); } return s.toString(); } /** * Generate the type of a parameter, i.e. a reference to a classifier. * * @param cls the classifier * @return the generated text */ private String generateClassifierRef(Object cls) { if (cls == null) { return ""; } return Model.getFacade().getName(cls); } }