View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.jetspeed.portlets.layout;
18  
19  import java.io.Serializable;
20  import java.text.DecimalFormat;
21  import java.text.DecimalFormatSymbols;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.SortedMap;
31  import java.util.TreeMap;
32  
33  import org.apache.jetspeed.om.page.Fragment;
34  
35  /***
36   * <h2>Basics</h2>
37   * <p>
38   * <code>ColumnLayout</code> is the model used to support any 1 to <i>n</i>
39   * column-based layout. <code>ColumnLayout</code> is constrained by a number
40   * columns that will not be exceeded, even if a fragment specifies a column
41   * outside of this constraint. Any fragment exceeded the specified column
42   * constraint will be deposited into the right-most column.
43   * </p>
44   * 
45   * <h2>Characteristics:</h2>
46   * <ul>
47   *   <li>Columns and rows always start at 0.</li>
48   *   <li>Unless otherwise noted, assume all Collections returned are immutable.</li>
49   *   <li>Unless otherwise noted, assume that no public method will ever return <code>null</code>.</li>
50   * </ul>
51   *  
52   * 
53   * <h2>Layout Events</h2>
54   * <p>
55   * When any move*() method is invoked and a portlet is actually moved (see indvidual
56   * methods for what causes these circumstances), an initial LayoutEvent is dispatched.  
57   * This may cause a cascade of LayoutEvents to be fired in turn if the movement of the
58   * target fragment cause other fragments to be repositioned.  In this case a LayoutEvent
59   * is dispatched for each portlet moved, which in turn may our may not cause another
60   * LayoutEvent to be fired. 
61   * </p>
62   * @see org.apache.jetspeed.portlets.layout.LayoutEvent
63   * @see org.apache.jetspeed.portlets.layout.LayoutEventListener
64   * @see org.apache.jetspeed.portlets.layout.LayoutCoordinate
65   * @see org.apache.jetspeed.om.page.Fragment
66   * 
67   * @author <href a="mailto:weaver@apache.org">Scott T. Weaver</a>
68   * 
69   */
70  public class ColumnLayout implements Serializable
71  {
72      /*** Percentage widths gutter width */
73      private final static double PERCENTAGE_WIDTH_GUTTER = 0.01;
74  
75      /*** Percentage widths format */
76      private final static DecimalFormat PERCENTAGE_WIDTH_FORMAT = new DecimalFormat("0.00'%'", new DecimalFormatSymbols(
77                                                                                 Locale.ENGLISH));
78  
79      /*** Constrains the columns for this layout */
80      private final int numberOfColumns;
81      
82      /*** SortedMap of Columns (which are also sorted maps */
83      private final SortedMap columns;
84      
85      /*** Width settings for eacah column */
86      private final String[] columnWidths;
87      
88      /*** Efficent way to always be aware of the next available row in a column */
89      private final int[] nextRowNumber;
90      
91      /*** maps Fragments (key) to it's current LayoutCoordinate (value) in this layout */
92      private final Map coordinates;
93      
94      /*** All of the LayoutEventListeners registered to this layout */
95      private final List eventListeners;
96  
97      /***
98       * 
99       * @param numberOfColumns
100      *            the maximum number of columns this layout will have.
101      * @param layoutType
102      *            this value corresponds to the property settings of the
103      *            fragments within your psml. Layout type allows segration of
104      *            property settings based on the type of layout in use. This
105      *            effectively allows for the interchange of multiple layout
106      *            formats without one format effecting the settings of another.
107      * @param columnWidths
108      *            widths for each column that accumulate to 100% if percentages
109      *            are used.
110      * @see org.apache.jetspeed.om.page.Fragment#getType()
111      */
112     public ColumnLayout(int numberOfColumns, String layoutType, String[] columnWidths)
113     {
114         this.numberOfColumns = numberOfColumns;
115         this.columnWidths = columnWidths;
116         eventListeners = new ArrayList();
117 
118         columns = new TreeMap();
119         coordinates = new HashMap();
120 
121         for (int i = 0; i < numberOfColumns; i++)
122         {
123             columns.put(new Integer(i), new TreeMap());
124         }
125 
126         nextRowNumber = new int[numberOfColumns];
127 
128         for (int i = 0; i < numberOfColumns; i++)
129         {
130             nextRowNumber[i] = 0;
131         }
132     }
133     
134     /***
135      * Same as ColumnLayout(int numberOfColumns, String layoutType) but also
136      * supplies a Collection of fragmetns to initially populate the layout
137      * with.  Adding these fragments <strong>WILL NOT</strong> cause
138      * a LayoutEvent to be dispatched.
139      * 
140      * @see ColumnLayout(int numberOfColumns, String layoutType)
141      * @param numberOfColumns
142      *            the maximum number of columns this layout will have.
143      * @param layoutType
144      *            this value corresponds to the property settings of the
145      *            fragments within your psml. Layout type allows segration of
146      *            property settings based on the type of layout in use. This
147      *            effectively allows for the interchange of multiple layout
148      *            formats without one format effecting the settings of another.
149      * @param fragments Initial set of fragments to add to this layout.
150      * @param columnWidths
151      *            widths for each column that accumulate to 100% if percentages
152      *            are used.
153      * @throws LayoutEventException
154      */
155     public ColumnLayout(int numberOfColumns, String layoutType, Collection fragments, String[] columnWidths) throws LayoutEventException
156     {
157         this(numberOfColumns, layoutType, columnWidths);
158         Iterator fragmentsItr = fragments.iterator();
159         try
160         {
161             while (fragmentsItr.hasNext())
162             {
163                 Fragment fragment = (Fragment) fragmentsItr.next();
164                 doAdd(getColumn(fragment), getRow(getColumn(fragment), fragment), fragment);
165             }
166         }
167         catch (InvalidLayoutLocationException e)
168         {
169             // This should NEVER happen as getColumn() should
170             // automatically constrain any fragments who's column
171             // setting would cause this exception.
172             throw new LayoutError("A malformed fragment could not be adjusted.", e);
173         }
174     }
175 
176     /***
177      * <p>
178      * Adds a fragment to the layout using fragment properties of
179      * <code> row  </code> and <code> column  </code> as hints on where to put
180      * this fragment. The following rules apply to malformed fragment
181      * definitions:
182      * </p>
183      * <ul>
184      * <li>Fragments without a row defined are placed at the bottom of their
185      * respective column </li>
186      * <li>Fragments without a column are placed in the right-most column.
187      * </li>
188      * <li> Fragments with overlapping row numbers. The last fragment has
189      * priority pushing the fragment in that row down one row. </li>
190      * </ul>
191      * 
192      * @param fragment
193      *            Fragment to add to this layout.
194      * @throws LayoutEventException 
195      * @see org.apache.jetspeed.om.page.Fragment
196      * 
197      */
198     public void addFragment(Fragment fragment) throws LayoutEventException
199     {
200         try
201         {
202             doAdd(getColumn(fragment), getRow(getColumn(fragment), fragment), fragment);
203             LayoutCoordinate coordinate = getCoordinate(fragment);
204             processEvent(new LayoutEvent(LayoutEvent.ADDED, fragment, coordinate, coordinate));
205         }
206         catch (InvalidLayoutLocationException e)
207         {
208             // This should NEVER happen as getColumn() should
209             // automatically constrain any fragments who's column
210             // setting would cause this exception.
211             throw new LayoutError("A malformed fragment could not be adjusted.", e);
212         }
213         catch (FragmentNotInLayoutException e)
214         {        
215             throw new LayoutError("Failed to add coordinate to this ColumnLayout.", e);
216         }
217     }
218     
219     /***
220      * Adds a LayoutEventListener to this layout that will be fired any time
221      * a LayoutEvent is disaptched.
222      * 
223      * @param eventListener
224      * @see LayoutEventListener
225      * @see LayoutEventListener
226      */
227     public void addLayoutEventListener(LayoutEventListener eventListener)
228     {
229         eventListeners.add(eventListener);
230     }
231 
232     /***
233      * 
234      * @param columnNumber
235      *            Number of column to retreive
236      * @return requested column (as a immutable Collection). Never returns
237      *         <code>null.</code>
238      * @throws InvalidLayoutLocationException
239      *             if the column is outisde of the constraints of this layout
240      */
241     public Collection getColumn(int columnNumber) throws InvalidLayoutLocationException
242     {
243         return Collections.unmodifiableCollection(getColumnMap(columnNumber).values());
244     }
245 
246     /***
247      * returns the width to be used with the specified column.  If
248      * there is no specific column setting sfor the specified column
249      * 0 is returned.
250      * 
251      * @param columnNumber whose width has been requested.
252      * @return the width to be used with the specified column.  Or 0 if no value
253      * has been specified.
254      */
255     public String getColumnWidth(int columnNumber)
256     {
257         if ((columnWidths != null) && (columnNumber < numberOfColumns))
258         {
259             String columnWidth = columnWidths[columnNumber];
260 
261             // subtract "gutter" width from last percentage
262             // column to prevent wrapping on rounding errors
263             // of column widths when rendered in the browser
264             if ((numberOfColumns > 1) && (columnNumber == (numberOfColumns - 1)))
265             {
266                 int percentIndex = columnWidth.lastIndexOf('%');
267                 if (percentIndex > 0)
268                 {
269                     try
270                     {
271                         double width = Double.parseDouble(columnWidth.substring(0,percentIndex).trim());
272                         synchronized (PERCENTAGE_WIDTH_FORMAT)
273                         {
274                             columnWidth = PERCENTAGE_WIDTH_FORMAT.format(width - PERCENTAGE_WIDTH_GUTTER);
275                         }
276                     }
277                     catch (NumberFormatException nfe)
278                     {
279                     }
280                 }
281             }
282             return columnWidth;
283         }
284         return "0";
285     }
286     
287     /***
288      * returns the float to be used with the specified column.
289      * 
290      * @param columnNumber whose width has been requested.
291      * @return "right" for the last column, "left" if more than one
292      *         column, or "none" otherwise.
293      */
294     public String getColumnFloat(int columnNumber)
295     {
296         if ((numberOfColumns > 1) && (columnNumber < numberOfColumns))
297         {
298             if (columnNumber == (numberOfColumns - 1))
299             {
300                 return "right";
301             }
302             else
303             {
304                 return "left";
305             }
306         }
307         return "none";
308     }
309     
310     /***
311      * @return <code>java.util.Collection</code> all of columns (also
312      *         Collection objects) in order within this layout. All Collections
313      *         are immutable.
314      */
315     public Collection getColumns()
316     {
317         ArrayList columnList = new ArrayList(getNumberOfColumns());
318         Iterator itr = columns.values().iterator();
319         while (itr.hasNext())
320         {
321             columnList.add(Collections.unmodifiableCollection(((Map) itr.next()).values()));
322         }
323 
324         return Collections.unmodifiableCollection(columnList);
325     }
326     
327     /***
328      * 
329      * Returns the index of the last row in the specified column.
330      * 
331      * @param columnNumber column form whom we ant to identify the
332      * last row.
333      * @return the index of the last row in the specified column.
334      */
335     public int getLastRowNumber(int columnNumber)
336     {
337         return nextRowNumber[columnNumber] - 1;
338     }
339     
340     /***
341      * Returns an immutable Collection of all the Fragments contained within
342      * this ColumnLayout in no sepcific order.
343      * @return Immutable Collection of Fragments.
344      */
345     public Collection getFragments()
346     {
347         return Collections.unmodifiableCollection(coordinates.keySet());
348     }
349 
350     /***
351      * Retrieves the fragment at the specified loaction.
352      * 
353      * @param columnNumber Column coordinate (first column starts at 0)
354      * @param rowNumber Row coordinate (first row starts at 0)
355      * @return Fragment at the specified coordinate.  Never returns <code>null</code>.
356      * @throws EmptyLayoutLocationException if there is no fragment currently located at the specified coordinate.
357      * @throws InvalidLayoutLocationException if the coordinate lies outside the confines of this layout, i.e., the
358      * <code>columnNumber</code> exceeds the max columns setting for this layout.
359      */
360     public Fragment getFragmentAt(int columnNumber, int rowNumber) throws EmptyLayoutLocationException,
361             InvalidLayoutLocationException
362     {
363         SortedMap column = getColumnMap(columnNumber);
364         Integer rowInteger = new Integer(rowNumber);
365         if (column.containsKey(rowInteger))
366         {
367             return (Fragment) column.get(rowInteger);
368         }
369         else
370         {
371             throw new EmptyLayoutLocationException(columnNumber, rowNumber);
372         }
373     }
374     
375     /***
376      * 
377      * Retrieves the fragment at the specified loaction.
378      * 
379      * @param coodinate LayoutCoordinate object that will be used to located a fragment in this
380      * layout.
381      * @see LayoutCoordinate
382      * @return Fragment at the specified coordinate.  Never returns <code>null</code>.
383      * @throws EmptyLayoutLocationException if there is no fragment currently located at the specified coordinate.
384      * @throws InvalidLayoutLocationException if the coordinate lies outside the confines of this layout, i.e., the
385      * <code>columnNumber</code> exceeds the max columns setting for this layout.
386      * @see LayoutCoordinate
387      */
388     public Fragment getFragmentAt(LayoutCoordinate coodinate) throws EmptyLayoutLocationException,
389             InvalidLayoutLocationException
390     {
391         return getFragmentAt(coodinate.getX(), coodinate.getY());
392     }
393 
394     /***
395      * 
396      * @return The total number of columns in this layout.
397      */
398     public int getNumberOfColumns()
399     {
400         return numberOfColumns;
401     }
402 
403     /***
404      * 
405      * @return The last column in this layout.  The Collection is immutable.
406      */
407     public Collection getLastColumn() 
408     {
409         try
410         {
411             return Collections.unmodifiableCollection(getColumnMap(numberOfColumns - 1).values());
412         }
413         catch (InvalidLayoutLocationException e)
414         {
415             // This should NEVER happen as getLastColumn() is
416             // always correctly constrained and should always exists.
417             throw new LayoutError("It appears this layout is corrupt and cannot correctly identify its last column.", e);
418         }
419     }
420 
421     /***
422      * 
423      * @return The last column in this layout.  The Collection is immutable.
424      */
425     public Collection getFirstColumn()
426     {
427         try
428         {
429             return Collections.unmodifiableCollection(getColumnMap(0).values());
430         }
431         catch (InvalidLayoutLocationException e)
432         {
433             // This should NEVER happen as getLastColumn() is
434             // always correctly constrained and should always exists.
435             throw new LayoutError("It appears this layout is corrupt and cannot correctly identify its first column.", e);
436         }
437     }
438 
439     /***
440      * 
441      * Moves a fragment one column to the right.  A LayoutEvent is triggered by
442      * this action.
443      * 
444      * <p>
445      * If the fragment currently
446      * resides in right-most column, no action is taking and no event LayoutEvent
447      * is fired.
448      * </p>
449      * 
450      * @param fragment fragment to move.
451      * @throws FragmentNotInLayoutException if the specified fragment is not currently in the layout.
452      * @throws LayoutEventException If a triggered LayoutEvent fails.
453      */
454     public void moveRight(Fragment fragment) throws FragmentNotInLayoutException, LayoutEventException
455     {
456         LayoutCoordinate coordinate = getCoordinate(fragment);
457         LayoutCoordinate newCoordinate = new LayoutCoordinate(coordinate.getX() + 1, coordinate.getY());
458 
459         if (newCoordinate.getX() < numberOfColumns)
460         {
461 
462             try
463             {
464                 doMove(fragment, coordinate, newCoordinate);
465                 processEvent(new LayoutEvent(LayoutEvent.MOVED_RIGHT, fragment, coordinate, newCoordinate));
466                 // now move the fragment below up one level.
467                 try
468                 {
469                     Fragment fragmentBelow = getFragmentAt(new LayoutCoordinate(coordinate.getX(), coordinate.getY() + 1));
470                     moveUp(fragmentBelow);
471                 }
472                 catch (EmptyLayoutLocationException e)
473                 {
474                     // indicates no fragment below
475                 }
476             }
477             catch (InvalidLayoutLocationException e)
478             {
479                 // This should NEVER happen as the location has already been verfied to be valid
480                 throw new LayoutError("It appears this layout is corrupt and cannot correctly identify valid column locations.", e);
481             }      
482         }
483     }
484 
485     /***
486      * Moves a fragment one column to the left.  A LayoutEvent is triggered by
487      * this action.
488      * 
489      * <p>
490      * If the fragment currently
491      * resides in left-most column, no action is taking and no event LayoutEvent
492      * is fired.
493      * </p>
494      * 
495      * @param fragment
496      * @throws FragmentNotInLayoutException if the specified fragment is not currently in the layout.
497      * @throws LayoutEventException If a triggered LayoutEvent fails.
498      */
499     public void moveLeft(Fragment fragment) throws FragmentNotInLayoutException, LayoutEventException
500     {
501         LayoutCoordinate coordinate = getCoordinate(fragment);
502         LayoutCoordinate newCoordinate = new LayoutCoordinate(coordinate.getX() - 1, coordinate.getY());
503 
504         if (newCoordinate.getX() >= 0)
505         {
506             try
507             {
508                 doMove(fragment, coordinate, newCoordinate);
509                 processEvent(new LayoutEvent(LayoutEvent.MOVED_LEFT, fragment, coordinate, newCoordinate));
510                 // now move the fragment below up one level.
511                 try
512                 {
513                     Fragment fragmentBelow = getFragmentAt(new LayoutCoordinate(coordinate.getX(), coordinate.getY() + 1));
514                     moveUp(fragmentBelow);
515                 }
516                 catch (EmptyLayoutLocationException e)
517                 {
518                     // indicates no fragment below
519                 }
520             }
521             catch (InvalidLayoutLocationException e)
522             {
523                 // This should NEVER happen as the location has already been verfied to be valid
524                 throw new LayoutError("It appears this layout is corrupt and cannot correctly identify valid column locations.", e);
525             }
526          
527         }
528 
529     }
530 
531     /***
532      * Moves a fragment one row to the up.  A LayoutEvent is triggered by
533      * this action.
534      * 
535      * <p>
536      * If the fragment currently
537      * resides in top-most row, no action is taking and no event LayoutEvent
538      * is fired.
539      * </p>
540      * @param fragment
541      * @throws FragmentNotInLayoutException if the specified fragment is not currently in the layout.
542      * @throws LayoutEventException If a triggered LayoutEvent fails.
543      */
544     public void moveUp(Fragment fragment) throws FragmentNotInLayoutException, LayoutEventException
545     {
546         LayoutCoordinate coordinate = getCoordinate(fragment);
547         LayoutCoordinate aboveLayoutCoordinate = new LayoutCoordinate(coordinate.getX(), coordinate.getY() - 1);
548         LayoutCoordinate newCoordinate = aboveLayoutCoordinate;
549 
550         // never go "above" 0.
551         if (newCoordinate.getY() >= 0)
552         {
553             try
554             {
555                 try
556                 {
557                     // now move the fragment above down one level.
558                     /*Fragment fragmentAbove =*/ getFragmentAt(aboveLayoutCoordinate);
559                     doMove(fragment, coordinate, newCoordinate);
560                     processEvent(new LayoutEvent(LayoutEvent.MOVED_UP, fragment, coordinate, newCoordinate));                
561                 }
562                 catch (EmptyLayoutLocationException e)
563                 {
564                     // Nothing above??? Then scoot all elements below up one level.
565                     doMove(fragment, coordinate, newCoordinate);
566                     processEvent(new LayoutEvent(LayoutEvent.MOVED_UP, fragment, coordinate, newCoordinate));
567                     
568                     // If this the last row, make sure to update the next row pointer accordingly.
569                     if(coordinate.getY() == (nextRowNumber[coordinate.getX()] - 1))
570                     {
571                         nextRowNumber[coordinate.getX()] = coordinate.getX();
572                     }
573                     
574                     try
575                     {
576                         Fragment fragmentBelow = getFragmentAt(new LayoutCoordinate(coordinate.getX(),
577                                 coordinate.getY() + 1));
578                         moveUp(fragmentBelow);
579                     }
580                     catch (EmptyLayoutLocationException e1)
581                     {
582 
583                     }
584                 }
585             }
586             catch (InvalidLayoutLocationException e)
587             {
588                 // This should NEVER happen as the location has already been verfied to be valid
589                 throw new LayoutError("It appears this layout is corrupt and cannot correctly identify valid column locations.", e);
590             }
591         }
592     }
593 
594     /***
595      * 
596      * @param fragment
597      * @throws FragmentNotInLayoutException if the specified fragment is not currently in the layout.
598      * @throws LayoutEventException If a triggered LayoutEvent fails.
599      */
600     public void moveDown(Fragment fragment) throws FragmentNotInLayoutException, LayoutEventException
601     {
602         LayoutCoordinate coordinate = getCoordinate(fragment);
603         LayoutCoordinate newCoordinate = new LayoutCoordinate(coordinate.getX(), coordinate.getY() + 1);
604 
605         // never move past the current bottom row
606         if (newCoordinate.getY() < nextRowNumber[coordinate.getX()])
607         {
608             try
609             {
610                 try
611                 {
612                     // the best approach to move a fragment down is to actually move
613                     // its neighbor underneath up
614                     LayoutCoordinate aboveCoord = new LayoutCoordinate(coordinate.getX(), coordinate.getY() + 1);
615                     Fragment fragmentBelow = getFragmentAt(aboveCoord);
616                     doMove(fragmentBelow, aboveCoord, coordinate);
617                     processEvent(new LayoutEvent(LayoutEvent.MOVED_UP, fragmentBelow, aboveCoord, coordinate));
618                     // Since this logic path is a somewhat special case, the processing of the  MOVED_DOWN
619                     // event happens within the doAdd() method.
620                 }
621                 catch (EmptyLayoutLocationException e)
622                 {
623                     doMove(fragment, coordinate, newCoordinate);
624                     processEvent(new LayoutEvent(LayoutEvent.MOVED_DOWN, fragment, coordinate, newCoordinate));
625                 }
626             }
627             catch (InvalidLayoutLocationException e)
628             {
629                 // This should NEVER happen as the location has already been verfied to be valid
630                 throw new LayoutError("It appears this layout is corrupt and cannot correctly identify valid column locations.", e);
631             }
632  
633         }
634     }
635 
636     /***
637      * Performs the actual movement of a fragment.
638      * 
639      * 
640      * @param fragment
641      * @param oldCoordinate
642      * @param newCoordinate
643      * @throws InvalidLayoutLocationException
644      * @throws LayoutEventException 
645      */
646     protected void doMove(Fragment fragment, LayoutCoordinate oldCoordinate, LayoutCoordinate newCoordinate)
647             throws InvalidLayoutLocationException, LayoutEventException
648     {
649         SortedMap oldColumn = getColumnMap(oldCoordinate.getX());
650         oldColumn.remove(new Integer(oldCoordinate.getY()));
651         coordinates.remove(fragment);
652 
653         doAdd(newCoordinate.getX(), newCoordinate.getY(), fragment);
654     }
655 
656     /***
657      *
658      * 
659      * @param fragment fragment whose LayoutCoordinate we ant.
660      * @return LayoutCoordinate representing the current location of this
661      * Fragment within this layout.
662      * @throws FragmentNotInLayoutException if the Fragment is not present in this layout.
663      * @see LayoutCoordinate
664      */
665     public LayoutCoordinate getCoordinate(Fragment fragment) throws FragmentNotInLayoutException
666     {
667         if (coordinates.containsKey(fragment))
668         {
669             return (LayoutCoordinate) coordinates.get(fragment);
670         }
671         else
672         {
673             throw new FragmentNotInLayoutException(fragment);
674         }
675     }
676 
677     /***
678      * Adds a fragment at the indicated <code>columnNumber</code>
679      * and <code>rowNumber</code>.
680      * 
681      * @param columnNumber
682      * @param rowNumber
683      * @param fragment
684      * @throws InvalidLayoutLocationException if the coordinates are outside the bounds of this layout.
685      * @throws LayoutEventException id a LayoutEvent fails
686      */
687     protected void doAdd(int columnNumber, int rowNumber, Fragment fragment) throws InvalidLayoutLocationException, LayoutEventException
688     {
689         SortedMap column = getColumnMap(columnNumber);
690     
691         Integer rowInteger = new Integer(rowNumber);
692         LayoutCoordinate targetCoordinate = new LayoutCoordinate(columnNumber, rowNumber);
693         if (column.containsKey(rowInteger))
694         {
695             // If the row has something in it, push everythin down 1
696             Fragment existingFragment = (Fragment) column.get(rowInteger);
697             column.put(rowInteger, fragment);
698             coordinates.put(fragment, targetCoordinate);
699             doAdd(columnNumber, ++rowNumber, existingFragment);
700             
701             LayoutCoordinate oneDownCoordinate = new LayoutCoordinate(targetCoordinate.getX(), targetCoordinate.getY() + 1);
702             processEvent(new LayoutEvent(LayoutEvent.MOVED_DOWN, existingFragment, targetCoordinate, oneDownCoordinate));
703         }
704         else
705         {
706             column.put(rowInteger, fragment);
707             coordinates.put(fragment, targetCoordinate);
708             rowNumber++;
709             if(rowNumber > nextRowNumber[columnNumber])
710             {
711                 nextRowNumber[columnNumber] = rowNumber;
712             }
713         }
714     
715     }
716 
717     /***
718      * Retrieves this specified <code>columnNumber</code> as a
719      * SortedMap.
720      * 
721      * @param columnNumber
722      * @return
723      * @throws InvalidLayoutLocationException if the <code>columnNumber</code> resides
724      * outside the bounds of this layout.
725      */
726     protected final SortedMap getColumnMap(int columnNumber) throws InvalidLayoutLocationException
727     {
728         Integer columnNumberIneteger = new Integer(columnNumber);
729 
730         if (columns.containsKey(columnNumberIneteger))
731         {
732             return ((SortedMap) columns.get(columnNumberIneteger));
733         }
734         else
735         {
736             throw new InvalidLayoutLocationException(columnNumber);
737         }
738 
739     }
740 
741     /***
742      * Gets the row number of this fragment to looking the <code>layoutType</code>
743      * property <i>row</i>.  If this property is undefined, the bottom-most row
744      * number of <code>currentColumn</code> is returned.
745      * 
746      * @param currentColumn
747      * @param fragment
748      * @return valid row for this fragment within this layout.
749      */
750     protected final int getRow(int currentColumn, Fragment fragment)
751     {
752         String propertyValue = fragment.getProperty(Fragment.ROW_PROPERTY_NAME);
753         if (propertyValue != null)
754         {
755             return Integer.parseInt(propertyValue);
756         }
757         else
758         {
759             return nextRowNumber[currentColumn];
760         }
761 
762     }
763 
764     /***
765      * Gets the row number of this fragment to looking the <code>layoutType</code>
766      * property <i>column</i>. 
767      * 
768      * If the <i>column</i> is undefined or exceeds the constriants of this
769      * layout, the value returned is <code>numberOfColumns - 1</code>.  If the
770      * value is less than 0, 0 is returned.
771      * 
772      * 
773      * @param fragment
774      * @return
775      */
776     protected final int getColumn(Fragment fragment)
777     {
778         String propertyValue = fragment.getProperty(Fragment.COLUMN_PROPERTY_NAME);
779         if (propertyValue != null)
780         {
781             int columnNumber = Integer.parseInt(propertyValue);
782 
783             // Exceeded columns get put into the last column
784             if (columnNumber >= numberOfColumns)
785             {
786                 columnNumber = (numberOfColumns - 1);
787             }
788             // Columns less than 1 go in the first column
789             else if (columnNumber < 0)
790             {
791                 columnNumber = 0;
792             }
793 
794             return columnNumber;
795         }
796         else
797         {
798             return (numberOfColumns - 1);
799         }
800     }
801     
802     /***
803      * Dispatches a LayoutEvent to all LayoutEventListeners registered to this layout.
804      * 
805      * @param event
806      * @throws LayoutEventException if an error occurs while processing a the LayoutEvent.
807      */
808     protected final void processEvent(LayoutEvent event) throws LayoutEventException
809     {
810         Iterator itr = eventListeners.iterator();
811         while(itr.hasNext())
812         {
813             LayoutEventListener eventListener = (LayoutEventListener) itr.next();
814             eventListener.handleEvent(event);
815         }
816         
817     }
818 
819 }