001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.layout;
017    
018    import java.awt.Component;
019    import java.io.IOException;
020    import java.io.StreamTokenizer;
021    import java.io.StringReader;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.HashMap;
025    import java.util.HashSet;
026    import java.util.Iterator;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Set;
030    
031    import javax.swing.JComponent;
032    import javax.swing.JPanel;
033    
034    import org.springframework.richclient.application.Application;
035    import org.springframework.richclient.application.ApplicationServicesLocator;
036    import org.springframework.richclient.factory.ComponentFactory;
037    import org.springframework.richclient.util.CustomizableFocusTraversalPolicy;
038    import org.springframework.util.Assert;
039    import org.springframework.util.StringUtils;
040    
041    import com.jgoodies.forms.factories.FormFactory;
042    import com.jgoodies.forms.layout.CellConstraints;
043    import com.jgoodies.forms.layout.ColumnSpec;
044    import com.jgoodies.forms.layout.FormLayout;
045    import com.jgoodies.forms.layout.RowSpec;
046    
047    /**
048     * A panel builder that provides the capability to quickly build grid based
049     * forms. The builder allows for layout to be defined in a way that will be
050     * familiar to anyone used to HTML tables and JGoodies Forms. Key features:
051     * <ul>
052     * <li>unlike HTML, cells automatically span all empty columns to the right.
053     * This can be disabled by setting "colSpan=1"</li>
054     * <li>support for gap rows and columns. You don't need to keep track of gap
055     * rows or columns when specifying row or column spans</li>
056     * <li>need only define colSpec and rowSpec when it varies from the default.
057     * Save you having to work out column specs before you start laying out the form
058     * </li>
059     * <li>rows and columns can be aliased with a group ID which save you having to
060     * keep track of row or column indexes for grouping. This also makes grouping
061     * less fragile when the table layout changes</li>
062     * </ul>
063     * <strong>Example: </strong> <br> 
064     * <pre>
065     * TableLayoutBuilder table = new TableLayoutBuilder();
066     * table
067     *  .row()
068     *      .separator(&quot;General 1&quot;)
069     *  .row()
070     *      .cell(new JLabel(&quot;Company&quot;), &quot;colSpec=right:pref colGrId=labels&quot;)
071     *      .labelGapCol()
072     *      .cell(new JFormattedTextField())
073     *  .row()
074     *      .cell(new JLabel(&quot;Contact&quot;))
075     *      .cell(new JFormattedTextField())
076     *      .unrelatedGapRow()
077     *      .separator(&quot;Propeller&quot;)
078     *  .row()
079     *      .cell(new JLabel(&quot;PTI [kW]&quot;)).cell(new JFormattedTextField())
080     *      .unrelatedGapCol()
081     *      .cell(new JLabel(&quot;Description&quot;), &quot;colSpec=right:pref colGrId=labels&quot;)
082     *      .labelGapCol()
083     *      .cell(new JScrollPane(new JTextArea()), &quot;rowspan=3&quot;)
084     *  .row()
085     *      .cell(new JLabel(&quot;R [mm]&quot;))
086     *      .cell(new JFormattedTextField())
087     *      .cell()
088     *  .row()
089     *      .cell(new JLabel(&quot;D [mm]&quot;))
090     *      .cell(new JFormattedTextField())
091     *      .cell(); 
092     * table.getPanel();
093     * </pre>
094     * 
095     * @author oliverh
096     */
097    public class TableLayoutBuilder implements LayoutBuilder {
098    
099        public static final String DEFAULT_LABEL_ATTRIBUTES = "colGrId=label colSpec=left:pref";
100    
101        public static final String ALIGN = "align";
102    
103        public static final String VALIGN = "valign";
104    
105        public static final String ROWSPEC = "rowSpec";
106    
107        public static final String COLSPEC = "colSpec";
108    
109        public static final String ROWSPAN = "rowSpan";
110    
111        public static final String COLSPAN = "colSpan";
112    
113        public static final String ROWGROUPID = "rowGrId";
114    
115        public static final String COLGROUPID = "colGrId";
116        
117        /** Constant indicating column major focus traversal order. */
118        public static final int COLUMN_MAJOR_FOCUS_ORDER = 1;
119        
120        /** Constant indicating row major focus traversal order. */
121        public static final int ROW_MAJOR_FOCUS_ORDER = 2;
122    
123        private List rowSpecs = new ArrayList();
124    
125        private List rowOccupiers = new ArrayList();
126    
127        private List columnSpecs = new ArrayList();
128    
129        private Map gapCols = new HashMap();
130    
131        private Map gapRows = new HashMap();
132    
133        private Map rowGroups = new HashMap();
134    
135        private Map colGroups = new HashMap();
136    
137        private int[][] adjustedColGroupIndices;
138    
139        private int[][] adjustedRowGroupIndices;
140    
141        private Cell lastCC = null;
142    
143        private int maxColumns = 0;
144    
145        private int currentRow = -1;
146    
147        private int currentCol = 0;
148    
149        private List items = new ArrayList();
150    
151        private JPanel panel;
152    
153        private List focusOrder = null;
154    
155        private ComponentFactory componentFactory;
156    
157        /**
158         * Creates a new TableLayoutBuilder.
159         */
160        public TableLayoutBuilder() {
161            // this will cause the panel to be lazily
162            // created in the getPanel method
163            this.panel = null;
164        }
165    
166        /**
167         * Creates a new TableLayoutBuilder which will perform it's layout
168         * in the supplied JPanel. Note that any components that are already 
169         * contained by the panel will be removed.
170         */
171        public TableLayoutBuilder(JPanel panel) {
172            Assert.notNull(panel, "panel is required");
173            this.panel = panel;
174            panel.removeAll();
175        }
176    
177        /**
178         * Returns the {@link ComponentFactory}that this uses to create things like
179         * labels.
180         * 
181         * @return if not explicitly set, this uses the {@link Application}'s
182         */
183        public ComponentFactory getComponentFactory() {
184            if (componentFactory == null) {
185                componentFactory = (ComponentFactory)ApplicationServicesLocator.services().getService(ComponentFactory.class);
186            }
187            return componentFactory;
188        }
189    
190        /**
191         * Sets the {@link ComponentFactory}that this uses to create things like
192         * labels.
193         */
194        public void setComponentFactory(ComponentFactory componentFactory) {
195            this.componentFactory = componentFactory;
196        }
197    
198        /**
199         * Returns the current row (zero-based) that the builder is putting
200         * components in.
201         */
202        public int getCurrentRow() {
203            return currentRow == -1 ? 0 : currentRow;
204        }
205    
206        /**
207         * Returns the current column (zero-based) that the builder is putting
208         * components in.
209         */
210        public int getCurrentCol() {
211            return currentCol;
212        }
213    
214        /**
215         * Inserts a new row. No gap row is inserted  before this row.
216         */
217        public TableLayoutBuilder row() {
218            ++currentRow;
219            lastCC = null;
220            maxColumns = Math.max(maxColumns, currentCol);
221            currentCol = 0;
222            return this;
223        }
224    
225        /**
226         * Inserts a new row. A gap row with specified RowSpec will be inserted
227         * before this row.
228         */
229        public TableLayoutBuilder row(String gapRowSpec) {
230            return row(new RowSpec(gapRowSpec));
231        }
232    
233        /**
234         * Inserts a new row. A gap row with specified RowSpec will be inserted
235         * before this row.
236         */
237        public TableLayoutBuilder row(RowSpec gapRowSpec) {
238            row();
239            gapRows.put(new Integer(currentRow), gapRowSpec);
240            return this;
241        }
242    
243        /**
244         * Inserts a new row. A related component gap row will be inserted before
245         * this row.
246         */
247        public TableLayoutBuilder relatedGapRow() {
248            return row(FormFactory.RELATED_GAP_ROWSPEC);
249        }
250    
251        /**
252         * Inserts a new row. An unrelated component gap row will be inserted before
253         * this row.
254         */
255        public TableLayoutBuilder unrelatedGapRow() {
256            return row(FormFactory.UNRELATED_GAP_ROWSPEC);
257        }
258    
259        /**
260         * Inserts an empty cell at the current row/column.
261         */
262        public TableLayoutBuilder cell() {
263            return cell("");
264        }
265    
266        /**
267         * Inserts an empty cell at the current row/column. Attributes may be zero or
268         * more of rowSpec, columnSpec, colGrId and rowGrId.
269         */
270        public TableLayoutBuilder cell(String attributes) {
271            cellInternal(null, attributes);
272            return this;
273        }
274    
275        /**
276         * Inserts a component at the current row/column.
277         */
278        public TableLayoutBuilder cell(JComponent component) {
279            return cell(component, "");
280        }
281    
282        /**
283         * Inserts a component at the current row/column. Attributes may be zero or
284         * more of rowSpec, columnSpec, colGrId, rowGrId, align and valign.
285         */
286        public TableLayoutBuilder cell(JComponent component, String attributes) {
287            Cell cc = cellInternal(component, attributes);
288            lastCC = cc;
289            items.add(cc);
290            return this;
291        }
292    
293        /**
294         * Inserts a related component gap column.
295         */
296        public TableLayoutBuilder gapCol() {
297            return relatedGapCol();
298        }
299    
300        /**
301         * Inserts a gap column with the specified colSpec.
302         */
303        public TableLayoutBuilder gapCol(String colSpec) {
304            return gapCol(new ColumnSpec(colSpec));
305        }
306    
307        /**
308         * Inserts a gap column with the specified colSpec.
309         */
310        public TableLayoutBuilder gapCol(ColumnSpec colSpec) {
311            gapCols.put(new Integer(currentCol), colSpec);
312            return this;
313        }
314    
315        /**
316         * Inserts a label component gap column.
317         */
318        public TableLayoutBuilder labelGapCol() {
319            return gapCol(FormFactory.LABEL_COMPONENT_GAP_COLSPEC);
320        }
321    
322        /**
323         * Inserts a unrelated component gap column.
324         */
325        public TableLayoutBuilder unrelatedGapCol() {
326            return gapCol(FormFactory.UNRELATED_GAP_COLSPEC);
327        }
328    
329        /**
330         * Inserts a related component gap column.
331         */
332        public TableLayoutBuilder relatedGapCol() {
333            return gapCol(FormFactory.RELATED_GAP_COLSPEC);
334        }
335    
336        /**
337         * Inserts a separator with the given label.
338         * 
339         * @deprecated this is a layout builder, creating components should be done elsewhere, use cell() methods instead
340         */
341        public TableLayoutBuilder separator(String labelKey) {
342            return separator(labelKey, "");
343        }
344    
345        /**
346         * Inserts a separator with the given label. Attributes my be zero or more of
347         * rowSpec, columnSpec, colGrId, rowGrId, align and valign.
348         * 
349         * @deprecated this is a layout builder, creating components should be done elsewhere, use cell() methods instead
350         */
351        public TableLayoutBuilder separator(String labelKey, String attributes) {
352            Cell cc = cellInternal(getComponentFactory().createLabeledSeparator(labelKey), attributes);
353            lastCC = cc;
354            items.add(cc);
355            return this;
356        }
357    
358        /**
359         * Return true if there is a gap column to the left of the current cell
360         */
361        public boolean hasGapToLeft() {
362            return currentCol == 0 || gapCols.get(new Integer(currentCol)) != null;
363        }
364    
365        /**
366         * Return true if there is a gap row above of the current cell
367         */
368        public boolean hasGapAbove() {
369            return currentRow == 0 || gapRows.get(new Integer(currentRow)) != null;
370        }
371    
372        /**
373         * Creates and returns a JPanel with all the given components in it, using
374         * the "hints" that were provided to the builder.
375         * 
376         * @return a new JPanel with the components laid-out in it
377         */
378        public JPanel getPanel() {
379            if (panel == null) {
380                panel = getComponentFactory().createPanel();
381            }
382            insertMissingSpecs();
383            fixColSpans();
384            fillInGaps();
385            fillPanel();
386            
387            if( focusOrder != null ) {
388                installFocusOrder( focusOrder );
389            }
390    
391            return panel;
392        }
393    
394        private Cell cellInternal(JComponent component, String attributes) {
395            nextCol();
396            Map attributeMap = getAttributes(attributes);
397            RowSpec rowSpec = getRowSpec(getAttribute(ROWSPEC, attributeMap, ""));
398            if (rowSpec != null) {
399                setRowSpec(currentRow, rowSpec);
400            }
401            ColumnSpec columnSpec = getColumnSpec(getAttribute(COLSPEC, attributeMap, ""));
402            if (columnSpec != null) {
403                setColumnSpec(currentCol, columnSpec);
404            }
405            addRowGroup(getAttribute(ROWGROUPID, attributeMap, null));
406            addColGroup(getAttribute(COLGROUPID, attributeMap, null));
407    
408            Cell cc = createCell(component, attributeMap);
409            currentCol = cc.endCol < cc.startCol ? cc.startCol : cc.endCol;
410            markContained(cc);
411            return cc;
412        }
413    
414        private void addRowGroup(String groupId) {
415            if (StringUtils.hasText(groupId)) {
416                Set group = (Set)rowGroups.get(groupId);
417                if (group == null) {
418                    group = new HashSet();
419                    rowGroups.put(groupId, group);
420                }
421                group.add(new Integer(getCurrentRow()));
422            }
423        }
424    
425        private void addColGroup(String groupId) {
426            if (StringUtils.hasText(groupId)) {
427                Set group = (Set)colGroups.get(groupId);
428                if (group == null) {
429                    group = new HashSet();
430                    colGroups.put(groupId, group);
431                }
432                group.add(new Integer(currentCol));
433            }
434        }
435    
436        private void setRowSpec(int row, RowSpec rowSpec) {
437            if (row >= rowSpecs.size()) {
438                int missingSpecs = row - rowSpecs.size() + 1;
439                for (int i = 0; i < missingSpecs; i++) {
440                    rowSpecs.add(getDefaultRowSpec());
441                }
442            }
443            rowSpecs.set(row, rowSpec);
444        }
445    
446        private void setColumnSpec(int col, ColumnSpec columnSpec) {
447            col = col - 1;
448            if (col >= columnSpecs.size()) {
449                int missingSpecs = col - columnSpecs.size() + 1;
450                for (int i = 0; i < missingSpecs; i++) {
451                    columnSpecs.add(getDefaultColSpec());
452                }
453            }
454            columnSpecs.set(col, columnSpec);
455        }
456    
457        private RowSpec getRowSpec(String rowSpec) {
458            if (StringUtils.hasText(rowSpec))
459                return new RowSpec(rowSpec);
460            
461            return null;
462        }
463    
464        private ColumnSpec getColumnSpec(String columnSpec) {
465            if (StringUtils.hasText(columnSpec)) 
466                return new ColumnSpec(columnSpec);
467    
468            return null;
469        }
470    
471        private void nextCol() {
472            if (lastCC != null && lastCC.endCol < lastCC.startCol) {
473                lastCC.endCol = lastCC.startCol;
474                lastCC = null;
475            }
476            // make sure that the first row has been created
477            if (currentRow == -1) {
478                row();
479            }
480            // now find the first unoccupied column
481            do {
482                ++currentCol;
483            }
484            while (getOccupier(currentRow, currentCol) != null);
485        }
486    
487        private Cell getOccupier(int row, int col) {
488            List occupiers = getOccupiers(row);
489            if (col >= occupiers.size()) {
490                return null;
491            }
492            return (Cell)occupiers.get(col);
493        }
494    
495        private List getOccupiers(int row) {
496            if (row >= rowOccupiers.size()) {
497                int numMissingRows = (row - rowOccupiers.size()) + 1;
498                for (int i = 0; i < numMissingRows; i++) {
499                    rowOccupiers.add(new ArrayList());
500                }
501            }
502            return (List)rowOccupiers.get(row);
503        }
504    
505        private void markContained(Cell cc) {
506            setOccupier(cc, cc.startRow, cc.endRow, cc.startCol, cc.endCol < cc.startCol ? cc.startCol : cc.endCol);
507        }
508    
509        private void setOccupier(Cell occupier, int startRow, int endRow, int startCol, int endCol) {
510            for (int row = startRow; row <= endRow; row++) {
511                List occupiers = getOccupiers(row);
512                if (endCol >= occupiers.size()) {
513                    int numMissingCols = (endCol - occupiers.size()) + 1;
514                    for (int i = 0; i < numMissingCols; i++) {
515                        occupiers.add(null);
516                    }
517                }
518                for (int i = startCol; i <= endCol; i++) {
519                    occupiers.set(i, occupier);
520                }
521            }
522    
523        }
524    
525        private Cell createCell(JComponent component, Map attributes) {
526            String align = getAttribute(ALIGN, attributes, "default");
527            String valign = getAttribute(VALIGN, attributes, "default");
528            int colSpan;
529            try {
530                colSpan = Integer.parseInt(getAttribute(COLSPAN, attributes, "-1"));
531            }
532            catch (NumberFormatException e) {
533                throw new IllegalArgumentException("Attribute 'colspan' must be an integer.");
534            }
535            int rowSpan;
536            try {
537                rowSpan = Integer.parseInt(getAttribute(ROWSPAN, attributes, "1"));
538            }
539            catch (NumberFormatException e) {
540                throw new IllegalArgumentException("Attribute 'rowspan' must be an integer.");
541            }
542    
543            return new Cell(component, getCurrentCol(), getCurrentRow(), colSpan, rowSpan, align + "," + valign);
544        }
545    
546        private void fixColSpans() {
547            for (Iterator i = items.iterator(); i.hasNext();) {
548                Cell cc = (Cell)i.next();
549                if (cc.endCol < cc.startCol) {
550                    int endCol = cc.startCol;
551                    while (endCol < maxColumns && getOccupier(cc.startRow, endCol + 1) == null) {
552                        ++endCol;
553                    }
554                    cc.endCol = endCol;
555                }
556                markContained(cc);
557            }
558        }
559    
560        private void fillInGaps() {
561            List adjustedCols = new ArrayList();
562            int adjustedCol = 0;
563            for (int col = 0; col < maxColumns; col++, adjustedCol++) {
564                ColumnSpec colSpec = (ColumnSpec)gapCols.get(new Integer(col));
565                if (colSpec != null) {
566                    columnSpecs.add(adjustedCol, colSpec);
567                    adjustedCol++;
568                }
569                adjustedCols.add(new Integer(adjustedCol + 1));
570            }
571            List adjustedRows = new ArrayList();
572            int adjustedRow = 0;
573            int numRows = rowSpecs.size();
574            for (int row = 0; row < numRows; row++, adjustedRow++) {
575                RowSpec rowSpec = (RowSpec)gapRows.get(new Integer(row));
576                if (rowSpec != null) {
577                    rowSpecs.add(adjustedRow, rowSpec);
578                    adjustedRow++;
579                }
580                adjustedRows.add(new Integer(adjustedRow));
581            }
582            for (Iterator i = items.iterator(); i.hasNext();) {
583                Cell cc = (Cell)i.next();
584                cc.startCol = ((Integer)adjustedCols.get(cc.startCol - 1)).intValue();
585                cc.endCol = ((Integer)adjustedCols.get(cc.endCol - 1)).intValue();
586                cc.startRow = ((Integer)adjustedRows.get(cc.startRow)).intValue();
587                cc.endRow = ((Integer)adjustedRows.get(cc.endRow)).intValue();
588            }
589            adjustedColGroupIndices = new int[colGroups.size()][];
590            int groupsCount = 0;
591            for (Iterator i = colGroups.values().iterator(); i.hasNext();) {
592                Set group = (Set)i.next();
593                adjustedColGroupIndices[groupsCount] = new int[group.size()];
594                int groupCount = 0;
595                for (Iterator j = group.iterator(); j.hasNext();) {
596                    adjustedColGroupIndices[groupsCount][groupCount++] = ((Integer)adjustedCols.get(((Integer)j.next()).intValue() - 1)).intValue();
597                }
598                groupsCount++;
599            }
600    
601            adjustedRowGroupIndices = new int[rowGroups.size()][];
602            groupsCount = 0;
603            for (Iterator i = rowGroups.values().iterator(); i.hasNext();) {
604                Set group = (Set)i.next();
605                adjustedRowGroupIndices[groupsCount] = new int[group.size()];
606                int groupCount = 0;
607                for (Iterator j = group.iterator(); j.hasNext();) {
608                    adjustedRowGroupIndices[groupsCount][groupCount++] = ((Integer)adjustedRows.get(((Integer)j.next()).intValue() - 1)).intValue();
609                }
610                groupsCount++;
611            }
612        }
613    
614        /**
615         * Set the focus traversal order.
616         * @param order forcus traversal order. Must be one of {@link #COLUMN_MAJOR_FOCUS_ORDER}
617         * or {@link #ROW_MAJOR_FOCUS_ORDER}.
618         */
619        public void setFocusTraversalOrder( int traversalOrder ) {
620            Assert.isTrue( traversalOrder == COLUMN_MAJOR_FOCUS_ORDER || traversalOrder == ROW_MAJOR_FOCUS_ORDER,
621                "traversalOrder must be one of COLUMN_MAJOR_FOCUS_ORDER or ROW_MAJOR_FOCUS_ORDER");
622            
623            List focusOrder = new ArrayList(items.size());
624    
625            if( traversalOrder == ROW_MAJOR_FOCUS_ORDER ) {
626                for( int row=0; row < rowOccupiers.size() - 1; row++ ) {
627                    for( int col=0; col < maxColumns; col++ ) {
628                        Cell currentCell = getOccupier(row, col);
629                        if (currentCell != null && !focusOrder.contains(currentCell.getComponent())) {
630                            focusOrder.add(currentCell.getComponent());
631                        }
632                    }
633                }
634            } else if( traversalOrder == COLUMN_MAJOR_FOCUS_ORDER ) {
635                for( int col = 0; col < maxColumns; col++ ) {
636                    for( int row = 0; row < rowOccupiers.size() - 1; row++ ) {
637                        Cell currentCell = getOccupier( row, col );
638                        if( currentCell != null && !focusOrder.contains( currentCell.getComponent() ) ) {
639                            focusOrder.add( currentCell.getComponent() );
640                        }
641                    }
642                }
643            }
644            
645            setCustomFocusTraversalOrder( focusOrder );
646        }
647    
648        /**
649         * Set a custom focus traversal order using the provided list of components.
650         * @param focusOrder List of components in the order that focus should follow.
651         */
652        public void setCustomFocusTraversalOrder( List focusOrder ) {
653            this.focusOrder = focusOrder;
654        }
655    
656        /**
657         * Install the specified focus order.
658         * @param focusOrder List of components in the order that focus should follow.
659         */
660        protected void installFocusOrder( List focusOrder ) {
661            CustomizableFocusTraversalPolicy.installCustomizableFocusTraversalPolicy();
662            CustomizableFocusTraversalPolicy.customizeFocusTraversalOrder(panel, focusOrder);
663        }
664    
665        private void fillPanel() {
666            panel.setLayout(createLayout());
667            for (Iterator i = items.iterator(); i.hasNext();) {
668                Cell cc = (Cell)i.next();
669                panel.add((Component)cc.getComponent(), cc.getCellConstraints());
670            }
671        }
672    
673        private FormLayout createLayout() {
674            ColumnSpec[] columnSpecsArray = (ColumnSpec[])columnSpecs.toArray(new ColumnSpec[columnSpecs.size()]);
675            RowSpec[] rowSpecArray = (RowSpec[])rowSpecs.toArray(new RowSpec[rowSpecs.size()]);
676            FormLayout layout = new FormLayout(columnSpecsArray, rowSpecArray);
677            layout.setColumnGroups(adjustedColGroupIndices);
678            layout.setRowGroups(adjustedRowGroupIndices);
679            return layout;
680        }
681    
682        private void insertMissingSpecs() {
683            maxColumns = Math.max(maxColumns, currentCol);
684            if (columnSpecs.size() < maxColumns) {
685                setColumnSpec(maxColumns, getDefaultColSpec());
686            }
687    
688            if (rowSpecs.size() <= getCurrentRow()) {
689                setRowSpec(getCurrentRow(), getDefaultRowSpec());
690            }
691        }
692    
693        private RowSpec getDefaultRowSpec() {
694            return FormFactory.DEFAULT_ROWSPEC;
695        }
696    
697        private ColumnSpec getDefaultColSpec() {
698            return new ColumnSpec("default:grow");
699        }
700    
701        private static final Set allowedAttributes;
702        static {
703            allowedAttributes = new HashSet();
704            allowedAttributes.add(COLSPAN.toLowerCase());
705            allowedAttributes.add(ROWSPAN.toLowerCase());
706            allowedAttributes.add(COLSPEC.toLowerCase());
707            allowedAttributes.add(ROWSPEC.toLowerCase());
708            allowedAttributes.add(ALIGN.toLowerCase());
709            allowedAttributes.add(VALIGN.toLowerCase());
710            allowedAttributes.add(ROWGROUPID.toLowerCase());
711            allowedAttributes.add(COLGROUPID.toLowerCase());
712        }
713    
714        private String getAttribute(String name, Map attributeMap, String defaultValue) {
715            String value = (String)attributeMap.get(name.toLowerCase());
716            if (value == null) {
717                value = defaultValue;
718            }
719            return value;
720        }
721    
722        private Map getAttributes(String attributes) {
723            if(!StringUtils.hasText(attributes))
724                return Collections.EMPTY_MAP;
725            
726            Map attributeMap = new HashMap();
727            try {
728                StreamTokenizer st = new StreamTokenizer(new StringReader(attributes));
729                st.resetSyntax();
730                st.wordChars(33, 126);
731                st.wordChars(128 + 32, 255);
732                st.whitespaceChars(0, ' ');
733                st.quoteChar('"');
734                st.quoteChar('\'');
735                st.ordinaryChar('=');
736    
737                String name = null;
738                boolean needEquals = false;
739    
740                while (st.nextToken() != StreamTokenizer.TT_EOF) {
741                    if (name == null && st.ttype == StreamTokenizer.TT_WORD) {
742                        name = st.sval;
743                        if (!allowedAttributes.contains(name.toLowerCase())) {
744                            throw new IllegalArgumentException("Attribute name '" + name + "' not recognised.");
745                        }
746                        needEquals = true;
747                    }
748                    else if (needEquals && st.ttype == '=') {
749                        needEquals = false;
750                    }
751                    else if (name != null && (st.ttype == StreamTokenizer.TT_WORD || st.ttype == '\'' | st.ttype == '"')) {
752                        attributeMap.put(name.toLowerCase(), st.sval);
753                        name = null;
754                    }
755                    else {
756                        throw new IllegalArgumentException("Expecting '=' but found '" + st.sval + "'");
757                    }
758                }
759                if (needEquals || name != null) {
760                    throw new IllegalArgumentException("Premature end of string. Expecting "
761                            + (needEquals ? " '='." : " value for attribute '" + name + "'."));
762                }
763            }
764            catch (IOException e) {
765                throw new UnsupportedOperationException("Encountered unexpected IOException. " + e.getMessage());
766            }
767    
768            return attributeMap;
769        }
770    
771        private static class Cell {
772            private JComponent component;
773    
774            private int startCol;
775    
776            private int startRow;
777    
778            private int endCol;
779    
780            private int endRow;
781    
782            private String align;
783    
784            public Cell(JComponent component, int x, int y, int w, int h, String align) {
785                this.component = component;
786                this.startCol = x;
787                this.startRow = y;
788                this.endCol = x + w - 1;
789                this.endRow = y + h - 1;
790                this.align = align;
791            }
792    
793            public Object getComponent() {
794                return component;
795            }
796    
797            public CellConstraints getCellConstraints() {
798                return new CellConstraints().xywh(startCol, startRow + 1, endCol - startCol + 1, endRow - startRow + 1,
799                        align);
800            }
801        }
802    }