Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
UMLMutableGraphSupport |
|
| 3.2962962962962963;3.296 |
1 | /* $Id: UMLMutableGraphSupport.java 17850 2010-01-12 19:53:35Z linus $ | |
2 | ***************************************************************************** | |
3 | * Copyright (c) 2009 Contributors - see below | |
4 | * All rights reserved. This program and the accompanying materials | |
5 | * are made available under the terms of the Eclipse Public License v1.0 | |
6 | * which accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | * | |
9 | * Contributors: | |
10 | * tfmorris | |
11 | ***************************************************************************** | |
12 | * | |
13 | * Some portions of this file was previously release using the BSD License: | |
14 | */ | |
15 | ||
16 | // Copyright (c) 1996-2007 The Regents of the University of California. All | |
17 | // Rights Reserved. Permission to use, copy, modify, and distribute this | |
18 | // software and its documentation without fee, and without a written | |
19 | // agreement is hereby granted, provided that the above copyright notice | |
20 | // and this paragraph appear in all copies. This software program and | |
21 | // documentation are copyrighted by The Regents of the University of | |
22 | // California. The software program and documentation are supplied "AS | |
23 | // IS", without any accompanying services from The Regents. The Regents | |
24 | // does not warrant that the operation of the program will be | |
25 | // uninterrupted or error-free. The end-user understands that the program | |
26 | // was developed for research purposes and is advised not to rely | |
27 | // exclusively on the program for any reason. IN NO EVENT SHALL THE | |
28 | // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, | |
29 | // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, | |
30 | // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF | |
31 | // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF | |
32 | // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY | |
33 | // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
34 | // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE | |
35 | // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF | |
36 | // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, | |
37 | // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
38 | ||
39 | package org.argouml.uml.diagram; | |
40 | ||
41 | import java.util.ArrayList; | |
42 | import java.util.Collection; | |
43 | import java.util.Dictionary; | |
44 | import java.util.Iterator; | |
45 | import java.util.List; | |
46 | import java.util.Map; | |
47 | ||
48 | import org.apache.log4j.Logger; | |
49 | import org.argouml.kernel.Project; | |
50 | import org.argouml.model.DiDiagram; | |
51 | import org.argouml.model.Model; | |
52 | import org.argouml.model.UmlException; | |
53 | import org.argouml.uml.CommentEdge; | |
54 | import org.tigris.gef.base.Editor; | |
55 | import org.tigris.gef.base.Globals; | |
56 | import org.tigris.gef.base.Mode; | |
57 | import org.tigris.gef.base.ModeManager; | |
58 | import org.tigris.gef.graph.MutableGraphSupport; | |
59 | ||
60 | ||
61 | /** | |
62 | * UMLMutableGraphSupport is a helper class which extends | |
63 | * MutableGraphSupport to provide additional helper and common methods | |
64 | * for UML Diagrams. | |
65 | * | |
66 | * @author mkl@tigris.org | |
67 | * @since November 14, 2002, 10:20 PM | |
68 | */ | |
69 | public abstract class UMLMutableGraphSupport extends MutableGraphSupport { | |
70 | ||
71 | /** | |
72 | * Logger. | |
73 | */ | |
74 | 900 | private static final Logger LOG = |
75 | Logger.getLogger(UMLMutableGraphSupport.class); | |
76 | ||
77 | private DiDiagram diDiagram; | |
78 | ||
79 | /** | |
80 | * Contains all the nodes in the graphmodel/diagram. | |
81 | */ | |
82 | 2116 | private List nodes = new ArrayList(); |
83 | ||
84 | /** | |
85 | * Contains all the edges in the graphmodel/diagram. | |
86 | */ | |
87 | 2116 | private List edges = new ArrayList(); |
88 | ||
89 | /** | |
90 | * The owning namespace or "home model" of this diagram, not all | |
91 | * ModelElements in this graph are in the home model, but if they are added | |
92 | * and don't already have a model, they are placed in the "home model". | |
93 | * Also, elements from other models will have their FigNodes add a line to | |
94 | * say what their model is. | |
95 | */ | |
96 | private Object homeModel; | |
97 | ||
98 | /** | |
99 | * The project this graph model is in. | |
100 | */ | |
101 | private Project project; | |
102 | ||
103 | /** | |
104 | * Constructor. | |
105 | * | |
106 | * @see org.tigris.gef.graph.MutableGraphSupport | |
107 | */ | |
108 | public UMLMutableGraphSupport() { | |
109 | 2116 | super(); |
110 | 2116 | } |
111 | ||
112 | /** | |
113 | * Get all the nodes from the graphmodel/diagram. | |
114 | * | |
115 | * @see org.tigris.gef.graph.MutableGraphSupport#getNodes() | |
116 | * @return List of nodes in the graphmodel/diagram | |
117 | */ | |
118 | public List getNodes() { | |
119 | 148 | return nodes; |
120 | } | |
121 | ||
122 | /** | |
123 | * Get all the edges from the graphmodel/diagram. | |
124 | * | |
125 | * @return List of edges in the graphmodel/diagram | |
126 | */ | |
127 | public List getEdges() { | |
128 | 148 | return edges; |
129 | } | |
130 | ||
131 | /* | |
132 | * @see org.tigris.gef.graph.MutableGraphModel#containsNode(java.lang.Object) | |
133 | */ | |
134 | public boolean containsNode(Object node) { | |
135 | 0 | return nodes.contains(node); |
136 | } | |
137 | ||
138 | /** | |
139 | * @param edge the candidate edge | |
140 | * @return true if it is contained | |
141 | */ | |
142 | public boolean constainsEdge(Object edge) { | |
143 | 0 | return edges.contains(edge); |
144 | } | |
145 | ||
146 | /** | |
147 | * Remove a node from the diagram and notify GEF. | |
148 | * | |
149 | * @param node node to remove | |
150 | */ | |
151 | @Override | |
152 | public void removeNode(Object node) { | |
153 | 0 | if (!containsNode(node)) { |
154 | 0 | return; |
155 | } | |
156 | 0 | nodes.remove(node); |
157 | 0 | fireNodeRemoved(node); |
158 | 0 | } |
159 | ||
160 | /** | |
161 | * Remove an edge from the graphmodel and notify GEF. | |
162 | * | |
163 | * @param edge edge to remove | |
164 | */ | |
165 | @Override | |
166 | public void removeEdge(Object edge) { | |
167 | 0 | if (!containsEdge(edge)) { |
168 | 0 | return; |
169 | } | |
170 | 0 | edges.remove(edge); |
171 | 0 | fireEdgeRemoved(edge); |
172 | 0 | } |
173 | ||
174 | /** | |
175 | * Assume that anything can be connected to anything unless overridden | |
176 | * in a subclass. | |
177 | * | |
178 | * {@inheritDoc} | |
179 | */ | |
180 | public boolean canConnect(Object fromP, Object toP) { | |
181 | 0 | return true; |
182 | } | |
183 | ||
184 | ||
185 | /** | |
186 | * The connect method without specifying a connection | |
187 | * type is unavailable in the ArgoUML implmentation. | |
188 | * | |
189 | * {@inheritDoc} | |
190 | */ | |
191 | public Object connect(Object fromPort, Object toPort) { | |
192 | 0 | throw new UnsupportedOperationException( |
193 | "The connect method is not supported"); | |
194 | } | |
195 | ||
196 | /** | |
197 | * Get the namespace, also known as homemodel, which owns the diagram. | |
198 | * | |
199 | * @return the homemodel | |
200 | */ | |
201 | public Object getHomeModel() { | |
202 | 0 | return homeModel; |
203 | } | |
204 | ||
205 | /** | |
206 | * Set the namespace or homemodel of the diagram. This will become the | |
207 | * default namespace for any model elements which are created on | |
208 | * the diagram. | |
209 | * | |
210 | * @param ns the namespace | |
211 | */ | |
212 | public void setHomeModel(Object ns) { | |
213 | 2097 | if (!Model.getFacade().isANamespace(ns)) { |
214 | 0 | throw new IllegalArgumentException(); |
215 | } | |
216 | 2097 | homeModel = ns; |
217 | 2097 | } |
218 | ||
219 | /** | |
220 | * The connect method specifying a connection | |
221 | * type by class is unavailable in the ArgoUML implementation. | |
222 | * TODO: This should be unsupported. Use the 3 Object version | |
223 | * | |
224 | * {@inheritDoc} | |
225 | */ | |
226 | public Object connect(Object fromPort, Object toPort, Class edgeClass) { | |
227 | 0 | return connect(fromPort, toPort, (Object) edgeClass); |
228 | } | |
229 | ||
230 | /** | |
231 | * Construct and add a new edge of the given kind and connect | |
232 | * the given ports. | |
233 | * | |
234 | * @param fromPort The originating port to connect | |
235 | * | |
236 | * @param toPort The destination port to connect | |
237 | * | |
238 | * @param edgeType The type of edge to create. This is one of the types | |
239 | * returned by the methods of | |
240 | * <code>org.argouml.model.MetaTypes</code> | |
241 | * | |
242 | * @return The type of edge created (the same as | |
243 | * <code>edgeClass</code> if we succeeded, | |
244 | * <code>null</code> otherwise) | |
245 | */ | |
246 | public Object connect(Object fromPort, Object toPort, Object edgeType) { | |
247 | // If this was an association then there will be relevant | |
248 | // information to fetch out of the mode arguments. If it | |
249 | // not an association then these will be passed forward | |
250 | // harmlessly as null. | |
251 | 0 | Editor curEditor = Globals.curEditor(); |
252 | 0 | ModeManager modeManager = curEditor.getModeManager(); |
253 | 0 | Mode mode = modeManager.top(); |
254 | 0 | Dictionary args = mode.getArgs(); |
255 | 0 | Object style = args.get("aggregation"); //MAggregationKind |
256 | 0 | Boolean unidirectional = (Boolean) args.get("unidirectional"); |
257 | 0 | Object model = getProject().getModel(); |
258 | ||
259 | // Create the UML connection of the given type between the | |
260 | // given model elements. | |
261 | // default aggregation (none) | |
262 | 0 | Object connection = |
263 | buildConnection( | |
264 | edgeType, fromPort, style, toPort, | |
265 | null, unidirectional, | |
266 | model); | |
267 | ||
268 | 0 | if (connection == null) { |
269 | 0 | if (LOG.isDebugEnabled()) { |
270 | 0 | LOG.debug("Cannot make a " + edgeType |
271 | + " between a " + fromPort.getClass().getName() | |
272 | + " and a " + toPort.getClass().getName()); | |
273 | } | |
274 | 0 | return null; |
275 | } | |
276 | ||
277 | 0 | addEdge(connection); |
278 | 0 | if (LOG.isDebugEnabled()) { |
279 | 0 | LOG.debug("Connection type" + edgeType |
280 | + " made between a " + fromPort.getClass().getName() | |
281 | + " and a " + toPort.getClass().getName()); | |
282 | } | |
283 | 0 | return connection; |
284 | } | |
285 | ||
286 | /** | |
287 | * Construct and add a new edge of the given kind and connect | |
288 | * the given ports. | |
289 | * | |
290 | * @param fromPort The originating port to connect | |
291 | * | |
292 | * @param toPort The destination port to connect | |
293 | * | |
294 | * @param edgeType An indicator of the edge type to create. | |
295 | * | |
296 | * @param styleAttributes key/value pairs from which to style the edge. | |
297 | * | |
298 | * @return The type of edge created (the same as | |
299 | * <code>edgeClass</code> if we succeeded, | |
300 | * <code>null</code> otherwise) | |
301 | */ | |
302 | public Object connect(Object fromPort, Object toPort, Object edgeType, | |
303 | Map styleAttributes) { | |
304 | 0 | return null; |
305 | } | |
306 | ||
307 | ||
308 | /* | |
309 | * @see org.tigris.gef.graph.MutableGraphModel#canAddNode(java.lang.Object) | |
310 | */ | |
311 | public boolean canAddNode(Object node) { | |
312 | 0 | if (node == null) { |
313 | 0 | return false; |
314 | } | |
315 | 0 | if (Model.getFacade().isAComment(node)) { |
316 | 0 | return true; |
317 | } | |
318 | 0 | return false; |
319 | } | |
320 | ||
321 | /** | |
322 | * Return the source end of an edge. | |
323 | * | |
324 | * @param edge The edge for which we want the source port. | |
325 | * | |
326 | * @return The source port for the edge, or <code>null</code> if the | |
327 | * edge given is of the wrong type or has no source defined. | |
328 | */ | |
329 | public Object getSourcePort(Object edge) { | |
330 | ||
331 | 0 | if (edge instanceof CommentEdge) { |
332 | 0 | return ((CommentEdge) edge).getSource(); |
333 | 0 | } else if (Model.getFacade().isARelationship(edge) |
334 | || Model.getFacade().isATransition(edge) | |
335 | || Model.getFacade().isAAssociationEnd(edge)) { | |
336 | 0 | return Model.getUmlHelper().getSource(edge); |
337 | 0 | } else if (Model.getFacade().isALink(edge)) { |
338 | 0 | return Model.getCommonBehaviorHelper().getSource(edge); |
339 | } | |
340 | ||
341 | // Don't know what to do otherwise | |
342 | ||
343 | 0 | LOG.error(this.getClass().toString() + ": getSourcePort(" |
344 | + edge.toString() + ") - can't handle"); | |
345 | ||
346 | 0 | return null; |
347 | } | |
348 | ||
349 | ||
350 | /** | |
351 | * Return the destination end of an edge. | |
352 | * | |
353 | * @param edge The edge for which we want the destination port. | |
354 | * | |
355 | * @return The destination port for the edge, or <code>null</code> if | |
356 | * the edge given is otf the wrong type or has no destination | |
357 | * defined. | |
358 | */ | |
359 | public Object getDestPort(Object edge) { | |
360 | 0 | if (edge instanceof CommentEdge) { |
361 | 0 | return ((CommentEdge) edge).getDestination(); |
362 | 0 | } else if (Model.getFacade().isAAssociation(edge)) { |
363 | 0 | List conns = new ArrayList(Model.getFacade().getConnections(edge)); |
364 | 0 | return conns.get(1); |
365 | 0 | } else if (Model.getFacade().isARelationship(edge) |
366 | || Model.getFacade().isATransition(edge) | |
367 | || Model.getFacade().isAAssociationEnd(edge)) { | |
368 | 0 | return Model.getUmlHelper().getDestination(edge); |
369 | 0 | } else if (Model.getFacade().isALink(edge)) { |
370 | 0 | return Model.getCommonBehaviorHelper().getDestination(edge); |
371 | } | |
372 | ||
373 | // Don't know what to do otherwise | |
374 | ||
375 | 0 | LOG.error(this.getClass().toString() + ": getDestPort(" |
376 | + edge.toString() + ") - can't handle"); | |
377 | ||
378 | 0 | return null; |
379 | } | |
380 | ||
381 | ||
382 | /* | |
383 | * @see org.tigris.gef.graph.MutableGraphModel#canAddEdge(java.lang.Object) | |
384 | */ | |
385 | public boolean canAddEdge(Object edge) { | |
386 | 0 | if (edge instanceof CommentEdge) { |
387 | 0 | CommentEdge ce = (CommentEdge) edge; |
388 | 0 | return isConnectionValid(CommentEdge.class, |
389 | ce.getSource(), | |
390 | ce.getDestination()); | |
391 | 0 | } else if (edge != null |
392 | && Model.getUmlFactory().isConnectionType(edge)) { | |
393 | 0 | return isConnectionValid(edge.getClass(), |
394 | Model.getUmlHelper().getSource(edge), | |
395 | Model.getUmlHelper().getDestination(edge)); | |
396 | } | |
397 | 0 | return false; |
398 | } | |
399 | ||
400 | /* | |
401 | * @see org.tigris.gef.graph.MutableGraphModel#addNodeRelatedEdges(java.lang.Object) | |
402 | */ | |
403 | public void addNodeRelatedEdges(Object node) { | |
404 | 0 | if (Model.getFacade().isAModelElement(node)) { |
405 | 0 | List specs = |
406 | new ArrayList(Model.getFacade().getClientDependencies(node)); | |
407 | 0 | specs.addAll(Model.getFacade().getSupplierDependencies(node)); |
408 | 0 | Iterator iter = specs.iterator(); |
409 | 0 | while (iter.hasNext()) { |
410 | 0 | Object dependency = iter.next(); |
411 | 0 | if (canAddEdge(dependency)) { |
412 | 0 | addEdge(dependency); |
413 | // return; | |
414 | } | |
415 | 0 | } |
416 | } | |
417 | ||
418 | // Commentlinks for comments. Iterate over all the comment links | |
419 | // to find the comment and annotated elements. | |
420 | ||
421 | 0 | Collection cmnt = new ArrayList(); |
422 | 0 | if (Model.getFacade().isAComment(node)) { |
423 | 0 | cmnt.addAll(Model.getFacade().getAnnotatedElements(node)); |
424 | } | |
425 | // TODO: Comments are on Element in UML 2.x | |
426 | 0 | if (Model.getFacade().isAModelElement(node)) { |
427 | 0 | cmnt.addAll(Model.getFacade().getComments(node)); |
428 | } | |
429 | 0 | Iterator iter = cmnt.iterator(); |
430 | 0 | while (iter.hasNext()) { |
431 | 0 | Object ae = iter.next(); |
432 | 0 | CommentEdge ce = new CommentEdge(node, ae); |
433 | 0 | if (canAddEdge(ce)) { |
434 | 0 | addEdge(ce); |
435 | } | |
436 | 0 | } |
437 | 0 | } |
438 | ||
439 | /** | |
440 | * Create an edge of the given type and connect it to the | |
441 | * given nodes. | |
442 | * | |
443 | * @param edgeType the UML object type of the connection | |
444 | * @param fromElement the UML object for the "from" element | |
445 | * @param fromStyle the aggregationkind for the connection | |
446 | * in case of an association | |
447 | * @param toElement the UML object for the "to" element | |
448 | * @param toStyle the aggregationkind for the connection | |
449 | * in case of an association | |
450 | * @param unidirectional for association and associationrole | |
451 | * @param namespace the namespace to use if it can't be determined | |
452 | * @return the newly build connection (UML object) | |
453 | */ | |
454 | protected Object buildConnection( | |
455 | Object edgeType, | |
456 | Object fromElement, | |
457 | Object fromStyle, | |
458 | Object toElement, | |
459 | Object toStyle, | |
460 | Object unidirectional, | |
461 | Object namespace) { | |
462 | ||
463 | 0 | Object connection = null; |
464 | 0 | if (edgeType == CommentEdge.class) { |
465 | 0 | connection = |
466 | buildCommentConnection(fromElement, toElement); | |
467 | } else { | |
468 | try { | |
469 | 0 | connection = |
470 | Model.getUmlFactory().buildConnection( | |
471 | edgeType, | |
472 | fromElement, | |
473 | fromStyle, | |
474 | toElement, | |
475 | toStyle, | |
476 | unidirectional, | |
477 | namespace); | |
478 | 0 | LOG.info("Created " + connection + " between " |
479 | + fromElement + " and " + toElement); | |
480 | 0 | } catch (UmlException ex) { |
481 | // fail silently as we expect users to accidentally drop | |
482 | // on to wrong component | |
483 | 0 | } catch (IllegalArgumentException iae) { |
484 | // idem, e.g. for a generalization with leaf/root object | |
485 | // TODO: but showing the message in the statusbar would help | |
486 | // TODO: IllegalArgumentException should not be used for | |
487 | // events we expect to happen. We need a different way of | |
488 | // catching well-formedness rules. | |
489 | 0 | LOG.warn("IllegalArgumentException caught", iae); |
490 | 0 | } |
491 | } | |
492 | 0 | return connection; |
493 | } | |
494 | ||
495 | /** | |
496 | * Builds the model behind a connection between a comment and | |
497 | * the annotated modelelement. | |
498 | * | |
499 | * @param from The comment or annotated element. | |
500 | * @param to The comment or annotated element. | |
501 | * @return A commentEdge representing the model behind the connection | |
502 | * between a comment and an annotated modelelement. | |
503 | */ | |
504 | public CommentEdge buildCommentConnection(Object from, Object to) { | |
505 | 0 | if (from == null || to == null) { |
506 | 0 | throw new IllegalArgumentException("Either fromNode == null " |
507 | + "or toNode == null"); | |
508 | } | |
509 | 0 | Object comment = null; |
510 | 0 | Object annotatedElement = null; |
511 | 0 | if (Model.getFacade().isAComment(from)) { |
512 | 0 | comment = from; |
513 | 0 | annotatedElement = to; |
514 | } else { | |
515 | 0 | if (Model.getFacade().isAComment(to)) { |
516 | 0 | comment = to; |
517 | 0 | annotatedElement = from; |
518 | } else { | |
519 | 0 | return null; |
520 | } | |
521 | } | |
522 | ||
523 | 0 | CommentEdge connection = new CommentEdge(from, to); |
524 | 0 | Model.getCoreHelper().addAnnotatedElement(comment, annotatedElement); |
525 | 0 | return connection; |
526 | ||
527 | } | |
528 | ||
529 | /** | |
530 | * Checks if some type of edge is valid to connect two | |
531 | * types of node. | |
532 | * | |
533 | * @param edgeType the UML object type of the connection | |
534 | * @param fromElement the UML object type of the "from" | |
535 | * @param toElement the UML object type of the "to" | |
536 | * @return true if valid | |
537 | */ | |
538 | protected boolean isConnectionValid( | |
539 | Object edgeType, | |
540 | Object fromElement, | |
541 | Object toElement) { | |
542 | ||
543 | 0 | if (!nodes.contains(fromElement) || !nodes.contains(toElement)) { |
544 | // The connection is not valid unless both nodes are | |
545 | // in this graph model. | |
546 | 0 | return false; |
547 | } | |
548 | ||
549 | 0 | if (edgeType.equals(CommentEdge.class)) { |
550 | 0 | return ((Model.getFacade().isAComment(fromElement) |
551 | && Model.getFacade().isAModelElement(toElement)) | |
552 | || (Model.getFacade().isAComment(toElement) | |
553 | && Model.getFacade().isAModelElement(fromElement))); | |
554 | } | |
555 | 0 | return Model.getUmlFactory().isConnectionValid( |
556 | edgeType, | |
557 | fromElement, | |
558 | toElement, | |
559 | true); | |
560 | } | |
561 | ||
562 | /** | |
563 | * Package scope. Only the factory is supposed to set this. | |
564 | * @param dd | |
565 | */ | |
566 | void setDiDiagram(DiDiagram dd) { | |
567 | 0 | diDiagram = dd; |
568 | 0 | } |
569 | ||
570 | /** | |
571 | * Get the object that represents this diagram | |
572 | * in the DiagramInterchangeModel. | |
573 | * | |
574 | * @return the Diagram Interchange Diagram. | |
575 | */ | |
576 | public DiDiagram getDiDiagram() { | |
577 | 0 | return diDiagram; |
578 | } | |
579 | ||
580 | /** | |
581 | * Return true if the current targets may be removed from the diagram. | |
582 | * | |
583 | * @param figs a collection with the selected figs | |
584 | * @return true if the targets may be removed | |
585 | */ | |
586 | public boolean isRemoveFromDiagramAllowed(Collection figs) { | |
587 | 836 | return !figs.isEmpty(); |
588 | } | |
589 | ||
590 | /** | |
591 | * Set the project that the graph model is inside. | |
592 | * @param p the project | |
593 | */ | |
594 | public void setProject(Project p) { | |
595 | 2116 | project = p; |
596 | 2116 | } |
597 | ||
598 | /** | |
599 | * Get the project that the graph model is inside. | |
600 | * @return the project | |
601 | */ | |
602 | public Project getProject() { | |
603 | 0 | return project; |
604 | } | |
605 | } |