FigMessage.java

/* $Id$
 *****************************************************************************
 * Copyright (c) 2009-2012 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:
 *    bobtarling
 *    Michiel van der Wulp
 *****************************************************************************
 *
 * Some portions of this file was previously release using the BSD License:
 */

// Copyright (c) 2007-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.sequence2.diagram;

import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JSeparator;

import org.argouml.model.Model;
import org.argouml.model.UmlChangeEvent;
import org.argouml.notation.Notation;
import org.argouml.notation.NotationProvider;
import org.argouml.notation.NotationProviderFactory2;
import org.argouml.notation.SDNotationSettings;
import org.argouml.ui.ArgoJMenu;
import org.argouml.uml.diagram.DiagramSettings;
import org.argouml.uml.diagram.ui.FigEdgeModelElement;
import org.argouml.uml.diagram.ui.FigTextGroup;
import org.argouml.uml.diagram.ui.PathItemPlacement;
import org.tigris.gef.base.Selection;
import org.tigris.gef.presentation.ArrowHead;
import org.tigris.gef.presentation.ArrowHeadGreater;
import org.tigris.gef.presentation.ArrowHeadTriangle;
import org.tigris.gef.presentation.Fig;
import org.tigris.gef.presentation.FigNode;
import org.tigris.gef.presentation.FigPoly;
import org.tigris.gef.presentation.FigText;

/**
 * The Fig that represents a message between classifier roles.
 * @author penyaskito
 */
public class FigMessage extends FigEdgeModelElement {

    private static final long serialVersionUID = -2961220746360335159L;

    private static final Logger LOG =
        Logger.getLogger(FigEdgeModelElement.class.getName());

    private FigTextGroup textGroup;

    /**
     * The action owned by the message
     */
    private Object action = null;

    private SDNotationSettings notationSettings;
    private boolean t[] = {
        false,
        false,
        false,
        false,
        false,
        false,
        false,
        false
    };

    /**
     * Construct a fig owned by the given UML element with the provided render
     * settings.
     * @param edge owning UML element
     * @param settings rendering settings
     */
    public FigMessage(Object edge, DiagramSettings settings) {
        super(edge, settings);
        textGroup = new FigTextGroup(edge, settings);
        initialize();
        action = Model.getFacade().getAction(getOwner());
        updateArrow();
        if (action != null) {
            addElementListener(action, "isAsynchronous");
        }
        t[1] = Model.getFacade().isASynchCallMessage(getOwner());
        t[2] = Model.getFacade().isAASynchCallMessage(getOwner());
        t[3] = Model.getFacade().isACreateMessage(getOwner());
        t[4] = Model.getFacade().isADeleteMessage(getOwner());
        t[5] = Model.getFacade().isAReplyMessage(getOwner());
        t[6] = Model.getFacade().isAASynchSignalMessage(getOwner());
    }

    private void initialize() {
        textGroup.addFig(getNameFig());
        textGroup.addFig(getStereotypeFig());
        addPathItem(textGroup, new PathItemPlacement(this, textGroup, 50, 10));
        notationSettings = new SDNotationSettings();
    }

    @Override
    protected int getNotationProviderType() {
        /* Use a different notation as Messages on a collaboration diagram: */
        return NotationProviderFactory2.TYPE_SD_MESSAGE;
    }

    /* This next argument may be used to switch off
     * the generation of sequence numbers - this is
     * still to be implemented.
     * They are less desired in sequence diagrams,
     * since they do not add any information.
     * In collaboration diagrams they are needed,
     * and they are still optional in sequence diagrams. */
    @Override
    protected void initNotationProviders(Object own) {
        super.initNotationProviders(own);
        notationSettings.setShowSequenceNumbers(false);
    }

    @Override
    protected void textEditStarted(FigText ft) {
        /* This is a temporary hack until the notation provider
         * for a SD Message will be able to parse successfully when the sequence
         * number is missing.
         * Remove this method completely then.*/
        notationSettings.setShowSequenceNumbers(true);
        super.textEditStarted(ft);
        notationSettings.setShowSequenceNumbers(false);
    }

    protected SDNotationSettings getNotationSettings() {
        return notationSettings;
    }

    boolean isSynchCallMessage() {
    	return t[1];
    }

    boolean isASynchCallMessage() {
        return t[2];
    }

    boolean isCreateMessage() {
    	return t[3];
    }

    boolean isDeleteMessage() {
    	return t[4];
    }

    boolean isReplyMessage() {
    	return t[5];
    }

    boolean isASynchSignalMessage() {
        return t[6];
    }

    /**
     * Updates the arrow head and the arrow line according
     * to the action type..
     */
    private void updateArrow() {
        if (getOwner() == null) {
            return;
        }
        getFig().setDashed(isReplyMessage());
        final Object act = getAction();
        final ArrowHead arrowHead;
        if (act != null && Model.getFacade().isAsynchronous(getAction())) {
            arrowHead = new ArrowHeadGreater();
        } else {
            arrowHead = new ArrowHeadTriangle();
            getFig().setFillColor(getLineColor());
        }
        setDestArrowHead(arrowHead);
    }

    /**
     * Gets the action attached to the message.
     * @return the action
     */
    private Object getAction() {
        return action;
    }

    /**
     * @param me the MouseEvent that triggered the popup menu request
     * @return a Vector containing a combination of these 4 types: Action,
     *         JMenu, JMenuItem, JSeparator.
     */
    @Override
    public Vector getPopUpActions(MouseEvent me) {
        Vector popUpActions = super.getPopUpActions(me);

        // Operations ...
        if (Model.getFacade().isACallAction(getAction())) {
            ArgoJMenu opMenu = buildOperationMenu();
            int index = popUpActions.size() - getPopupAddOffset() - 1;
            if (index < 0) {
                index = 0;
            }
            popUpActions.add(index, new JSeparator());
            popUpActions.add(index, opMenu);
        }

        return popUpActions;
    }

    protected ArgoJMenu buildOperationMenu() {
        ArgoJMenu opMenu = new ArgoJMenu("Operation");
        Iterator<Object> iter = getReceiverOperations().iterator();
        opMenu.setEnabled(iter.hasNext());
        while (iter.hasNext()) {
            Object op = iter.next();
            NotationProvider np = null;
            try {
                String s = getNotationSettings().getNotationLanguage();
                np = NotationProviderFactory2.getInstance()
                    .getNotationProvider(
                        NotationProviderFactory2.TYPE_OPERATION,
                        op,
                        Notation.findNotation(s));
            } catch (Exception e) {
                //TODO: add logging, but this will never happen and is handled
                np = null;
            }
            String label = (np != null)
                ? np.toString(op, getNotationSettings())
                : Model.getFacade().getName(op);
            opMenu.add(new ActionSetOperation(getAction(), op, label));
        }
        return opMenu;
    }

    @Override
    public Selection makeSelection() {
        return new SelectionMessage(this);
    }

    /*
     * @see org.tigris.gef.presentation.FigEdge#setFig(org.tigris.gef.presentation.Fig)
     */
    public void setFig(Fig f) {
        super.setFig(f);
        updateArrow();
    }

    int getFinalY() {
	int finalY = 0;
        Point[] points = getFig().getPoints();
        if (points.length > 0) {
            finalY = points[points.length - 1].y;
        }
        return finalY;
    }

    int getStartY() {
	int finalY = 0;
        Point[] points = getFig().getPoints();
        if (points.length > 0) {
            finalY = points[0].y;
        }
        return finalY;
    }


    /**
     * Checks if the message source and dest are the same.
     * @return true if they are the same, otherwise false.
     */
    boolean isSelfMessage() {
        // If possible we determine this by checking the destination
        // and source Figs are the same. If this is not possible
        // because the edge is not yet connected then we check the
        // model.
        if (getDestPortFig() == null || getSourcePortFig() == null) {
            return getDestination().equals(getSource());
        } else {
            return (getDestPortFig().equals(getSourcePortFig()));
        }
    }

    /**
     * Converts the message into a spline.
     * This is needed for self-referencing messages.
     */
    public void convertToArc() {
        if (getPoints().length > 0) {
            FigMessageSpline spline = new FigMessageSpline(getPoint(0));
            spline.setDashed(isReplyMessage());
            super.setFig(spline);
            computeRoute();
        }
    }

    @Override
    public void computeRouteImpl() {
	super.computeRouteImpl();
	updateActivations();
    }

    public void calcBounds() {
        final FigPoly fp = (FigPoly) getFig();
        final FigNode node = getSourceFigNode();
        if (node != null && isSelfMessage() && fp.isComplete()) {
            // TODO: calcBounds is called by SelectionManager when the Fig is
            // dragged. This code is needed to reposition any self message
            // as they are become detached from their classifier role
            // (see issue 5562). The cause of the detachment is not yet
            // understood.
            // Unfortunately calcBounds is called from several other places
            // so the code here is not optimal but is the best workaround until
            // ArgoUML can provide its own replacement SelectionManager for
            // sequence diagram requirements
            // See - http://gef.tigris.org/issues/show_bug.cgi?id=344
            final int x =
                node.getX()
                + (node.getWidth() + FigActivation.DEFAULT_WIDTH) / 2;
            final Point startPoint = new Point(x, getYs()[0]);
            final FigMessageSpline spline = new FigMessageSpline(startPoint);
            spline.setComplete(true);
            spline.setDashed(isReplyMessage());
            super.setFig(spline);
        }
        super.calcBounds();
    }

    private synchronized void updateActivations() {
	// we update the activations...
	FigClassifierRole source = (FigClassifierRole) getSourceFigNode();
	if (source != null) {
	    source.createActivations();
	}

	// for performance, we check if this is a selfmessage
	// if it is, we have just updated the activations
	if (!isSelfMessage()) {
	    FigClassifierRole dest = (FigClassifierRole) getDestFigNode();
	    if (dest != null) {
		dest.createActivations();
	    }
	}
    }

    @Override
    public void setLineColor(Color c) {
    	super.setLineColor(c);
    	getDestArrowHead().setFillColor(c);
    }

    @Override
    public void deleteFromModel() {
    	((FigClassifierRole) getSourceFigNode()).createActivations();
    	if (!isSelfMessage()) {
    	    ((FigClassifierRole) getDestFigNode()).createActivations();
    	}
    	super.deleteFromModel();
    }

    /**
     * The default behaviour from FigEdgeModelElement is not correct
     * here. See issue 5005. TODO: We must determine what to do here but for
     * now doing nothing is better. I'm not sure why the super method would
     * not work as I would expect that to do nothing if the ends are already
     * correct.
     * @return true at all times for now
     */
    @Override
    protected boolean determineFigNodes() {
        return true;
    }

    @Override
    public void translate(int dx, int dy) {
        if (isSelfMessage()) {
            ((FigMessageSpline) getFig()).translateFig(dx, dy);
        }
        super.translate(dx, dy);
    }

    /**
     * {@inheritDoc}
     * @see org.argouml.uml.diagram.ui.FigEdgeModelElement#updateLayout(org.argouml.model.UmlChangeEvent)
     */
    @Override
    public void updateLayout(UmlChangeEvent event) {
        if ("isAsynchronous".equals(event.getPropertyName())) {
            updateArrow();
        }
        super.updateLayout(event);
    }

    /*
     * Overridden purely to keep our superclass from removing the listener
     * that we just added.
     *
     * @see org.argouml.uml.diagram.ui.FigEdgeModelElement#updateListeners(java.lang.Object, java.lang.Object)
     */
    @Override
    protected void updateListeners(Object oldOwner, Object newOwner ) {
        action = Model.getFacade().getAction(newOwner);
        Set<Object[]> listeners = new HashSet<Object[]>();
        Object action = getAction();
        listeners.add(new Object[] {getOwner(), "remove"});
        if (action != null) {
            listeners.add(new Object[] {action, "isAsynchronous"});
        }
        try {
            updateElementListeners(listeners);
        } catch (Exception e) {
            // This call seems not very robust. Yet to determine cause.
            LOG.log(Level.SEVERE, "Exception caught", e);
        }
    }

    private Collection<Object> getReceiverOperations() {
        ArrayList<Object> opList = new ArrayList<Object>();
        Object action = getAction();
	    Object receiver = Model.getFacade().getReceiver(getOwner());
        if (action != null && receiver != null) {
            //TODO: What can we do with other kind of actions?
            if (Model.getFacade().isACallAction(action)) {
                Iterator bases =
                    Model.getFacade().getBases(receiver).iterator();
                while (bases.hasNext()) {
                    Object base = bases.next();
                    opList.addAll(Model.getFacade().getOperations(base));
                }
            }
        }
        return opList;
    }
    /**
     * Determines the activator of this message based on the message position
     * in relation to other messages.
     * <p>Currently this only manages return messages. Any other message type
     * returns with no action taking place.
     * <p>The activator is set to the first call or create message found above
     * this message.
     * @return the activator that has been applied to the message.
     */
    public Object determineActivator() {
        final FigClassifierRole fcr = (FigClassifierRole) getSourceFigNode();
        final List<FigMessage> messageFigs = fcr.getFigMessages();
        final Iterator<FigMessage> it = messageFigs.iterator();
        Object activator = null;
        while (it.hasNext()) {
            FigMessage messageFig = it.next();
            if ((messageFig.isCreateMessage() || messageFig.isSynchCallMessage())
                    && messageFig.getDestFigNode() == fcr) {
                activator = messageFig.getOwner();
            } else if (messageFig == this) {
                Model.getCollaborationsHelper().setActivator(
                        getOwner(), activator);
                return activator;
            } else if (messageFig.isReplyMessage()) {
                activator = null;
            }
        }
        return null;
    }
}