// $Id: TransitionNotationUml.java 16400 2008-12-21 16:27:58Z tfmorris $ // Copyright (c) 2005-2008 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.beans.PropertyChangeListener; import java.text.ParseException; import java.util.Collection; import java.util.Iterator; import java.util.Map; 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 mvw@tigris.org */ 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> * * If the last character of this line * is a ";", then it is ignored.<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 "/". * * @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)) { String msg = "parsing.error.transition.found-bracket-instead-slash"; throw new ParseException(Translator.localize(msg), 0); } StringTokenizer tokenizer = new StringTokenizer(s, "[/"); String eventSignature = null; String guardCondition = null; String actionExpression = null; while (tokenizer.hasMoreTokens()) { String nextToken = tokenizer.nextToken().trim(); if (nextToken.endsWith("]")) { guardCondition = nextToken.substring(0, nextToken.length() - 1); } else { if (s.startsWith(nextToken)) { eventSignature = nextToken; } else { if (s.endsWith(nextToken)) { actionExpression = nextToken; } } } } if (eventSignature != null) { // parseEventSignature(trans, eventSignature); parseTrigger(trans, eventSignature); } if (guardCondition != null) { parseGuard(trans, guardCondition.substring(guardCondition.indexOf('[') + 1)); } if (actionExpression != null) { parseEffect(trans, actionExpression.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. Create a new trigger, name it, 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 createdEvent = false; if (trigger.length() > 0) { // case 1 and 2 if (evt == null) { // case 1 if (timeEvent) { // after(...) evt = sMFactory.buildTimeEvent(s, ns); /* Do not set the name. */ } if (changeEvent) { // when(...) evt = sMFactory.buildChangeEvent(s, ns); /* Do not set the name. */ } if (callEvent) { // operation(paramlist) String triggerName = trigger.indexOf("(") > 0 ? trigger.substring(0, trigger.indexOf("(")).trim() : trigger; evt = sMFactory.buildCallEvent(trans, triggerName, ns); // and parse the parameter list NotationUtilityUml.parseParamList(evt, s, 0); } if (signalEvent) { // signalname evt = sMFactory.buildSignalEvent(trigger, ns); } createdEvent = true; } else { // case 2 if (timeEvent) { if (Model.getFacade().isATimeEvent(evt)) { /* Just change the time expression */ Object timeExpr = Model.getFacade().getWhen(evt); 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); createdEvent = 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); createdEvent = 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); createdEvent = 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); createdEvent = true; } } } if (createdEvent && (evt != null)) { Model.getStateMachinesHelper().setEventAsTrigger(trans, evt); } } else { // case 3 and 4 if (evt == null) { /* case 3 */ } else { // case 4 delete(evt); // erase it } } } /** * 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 mvw@tigris.org * @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); } /* * @see org.argouml.uml.notation.NotationProvider#toString(java.lang.Object, java.util.Map) */ @SuppressWarnings("deprecation") @Deprecated public String toString(Object modelElement, Map args) { 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(); } 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); } /* * @see org.argouml.uml.notation.NotationProvider#initialiseListener(java.beans.PropertyChangeListener, java.lang.Object) */ public void initialiseListener(PropertyChangeListener listener, Object modelElement) { addListenersForTransition(listener, modelElement); } private void addListenersForAction(PropertyChangeListener listener, Object action) { if (action != null) { addElementListener(listener, action, new String[] { // TODO: Action isn't a valid property name // Or is it? Double check validity checking code "script", "actualArgument", "action" }); Collection args = Model.getFacade().getActualArguments(action); Iterator i = args.iterator(); while (i.hasNext()) { Object argument = i.next(); addElementListener(listener, argument, "value"); } if (Model.getFacade().isAActionSequence(action)) { Collection subactions = Model.getFacade().getActions(action); i = subactions.iterator(); while (i.hasNext()) { Object a = i.next(); addListenersForAction(listener, a); } } } } private void addListenersForEvent(PropertyChangeListener listener, Object event) { if (event != null) { if (Model.getFacade().isAEvent(event)) { addElementListener(listener, event, new String[] { "parameter", "name"}); } if (Model.getFacade().isATimeEvent(event)) { addElementListener(listener, event, new String[] {"when"}); } if (Model.getFacade().isAChangeEvent(event)) { addElementListener(listener, event, new String[] {"changeExpression"}); } Collection prms = Model.getFacade().getParameters(event); Iterator i = prms.iterator(); while (i.hasNext()) { Object parameter = i.next(); addElementListener(listener, parameter); } } } private void addListenersForTransition(PropertyChangeListener listener, Object transition) { addElementListener(listener, transition, new String[] {"guard", "trigger", "effect"}); Object guard = Model.getFacade().getGuard(transition); if (guard != null) { addElementListener(listener, guard, "expression"); } Object trigger = Model.getFacade().getTrigger(transition); addListenersForEvent(listener, trigger); Object effect = Model.getFacade().getEffect(transition); addListenersForAction(listener, effect); } }