KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > xquark > mapper > mapping > TableMappingImpl


1 /*
2  * This file belongs to the XQuark distribution.
3  * Copyright (C) 2003 Universite de Versailles Saint-Quentin.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307.
18  * You can also get it at http://www.gnu.org/licenses/lgpl.html
19  *
20  * For more information on this software, see http://www.xquark.org.
21  */

22
23 package org.xquark.mapper.mapping;
24
25 import java.util.*;
26
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 import org.xml.sax.SAXException JavaDoc;
30 import org.xquark.jdbc.typing.ColumnMetaData;
31 import org.xquark.mapper.dbms.AbstractConnection;
32 import org.xquark.schema.ElementDeclaration;
33 import org.xquark.schema.SchemaComponent;
34 import org.xquark.schema.Type;
35
36 /**
37  * Implementation of the TableMapping interface.
38  *
39  * Every TableMapping object owns a set of ColumnMapping object corresponding
40  * to its table's columns.
41  *
42  */

43 class TableMappingImpl extends BaseTableMappingImpl
44 {
45     private static final String JavaDoc RCSRevision = "$Revision: 1.7 $";
46     private static final String JavaDoc RCSName = "$Name: $";
47     
48     private static final short USE_PK = 0;
49     private static final short USE_KEYGEN = 1;
50     private static final short USE_ALL = 2;
51     
52     private static Log log = LogFactory.getLog(TableMappingImpl.class);
53     
54     /* Set of TableMapping object which this TableMapping is dependent on */
55     
56     /* USE ? */
57     private HashSet mappedElements = new HashSet();
58     /* USE ? */
59     private HashSet endingElements = new HashSet();
60     /* String for JDBC PreparedStatements used to access the OID */
61     protected String JavaDoc selectStatement;
62     protected String JavaDoc updateStatement;
63     
64     private short joinMode = -1;
65         
66     /* Number of columns that are used in the where clause to retrieve rows */
67     private int keyGenColumnCount = 0;
68     private int selectColumnCount = 0;
69     private int fetchColumnCount = 0;
70     private int updateColumnCount = 0;
71     private int columnMappingCount = 0; // different from columnCount
72
private ColumnMappingImpl[] denseColumnMappingArray = null; // i.e., insert column mapping
73

74     private int action;
75     private boolean clustered = false; // if true, OID table is clusterd to the value table
76
private ColumnMappingImpl uoidColumn = null; // column corresponding to the universal OID if clustering
77
private ColumnMappingImpl pathColumn = null; // column corresponding to the path ID if clustering
78

79     private int batchSize; // JDBC batch size
80
/* This flag is used to desactivate OID tables in mapping for mapper */
81     private boolean userTablesOnly = false;
82     private boolean shared = false;
83     
84     /** Constructor.
85      * @param colMapping the RepositoryMapping object to which this object belongs.
86      * @param comp the SchemaComponent associated with this "mapping component".
87      * @param name relational table name.
88      * @param conn a connection allowing access to the metadata for the table retrieved from JDBC
89      * @param action the action to perform on the rows of the table (check, insert, update, ...)
90      * @param generate If true, mapping is loaded even if relational tables
91      * are not present : mapping is loaded for table generation.
92      */

93     TableMappingImpl(RepositoryMapping colMapping, SchemaComponent comp,
94     String JavaDoc tableName, AbstractConnection conn, int action, boolean generate,
95     int batchSize, boolean userTablesOnly) throws SAXException JavaDoc
96     {
97         super(colMapping, comp, conn);
98         this.name = tableName;
99         this.userTablesOnly = userTablesOnly;
100         tableMetaData = colMapping.getTableMetaData(name, conn);
101         shared = comp instanceof Type;
102         
103         // Check table existence
104
if (!generate && tableMetaData == null)
105             throw new SAXException JavaDoc("Table " + name + " does not exist in database.");
106             
107         if (tableMetaData == null)
108             // create column mapping array using abitrary size (no metadata)
109
columnMappings = new ColumnMapping[20];
110         else // create column mapping array
111
columnMappings = new ColumnMapping[tableMetaData.getColumnCount()];
112
113         this.action = action;
114         this.batchSize = batchSize;
115     }
116     
117     public int getBatchSize() { return batchSize;}
118     
119     public int getOIDTableColumnCount() { return OIDColumnMappings.length + 2;}
120     
121     public int getSelectColumnCount() { return selectColumnCount;}
122     
123     public int getFetchColumnCount() { return fetchColumnCount;}
124     
125     public int getKeyGenColumnCount() { return keyGenColumnCount;}
126     
127     public int getUpdateColumnCount() { return updateColumnCount;}
128     
129     public int getColumnMappingCount() { return columnMappingCount;}
130     
131     public ColumnMapping[] getColumnMappings() { return denseColumnMappingArray;}
132     
133     public int getAction() { return action;}
134     
135     /**
136      * Returns true if the OID table is clustered.
137      */

138     public boolean isClustered() { return clustered;}
139     
140     /**
141      * @see org.xquark.mapper.mapping.TableMapping#isShared()
142      */

143     public boolean isShared() { return shared;}
144
145     /**
146      * if clustered, return the OID column index.
147      */

148     public int getPathIDIndex()
149     {
150         if (pathColumn != null)
151             return pathColumn.getColumnIndex();
152         else
153             return 1;
154     }
155     
156     /**
157      * if clustered, return the root OID column index.
158      */

159     public int getUOIDIndex()
160     {
161         if (uoidColumn != null)
162             return uoidColumn.getColumnIndex();
163         else
164             return 0;
165     }
166     
167     /**
168      * Returns the name of the column containing the path OID (both in clustered
169      * and not clustered case).
170      */

171     public String JavaDoc getPathIDColumnName()
172     {
173         if (pathColumn != null)
174             return pathColumn.getColumnName();
175         else
176             return super.getPathIDColumnName();
177     }
178     
179     /**
180      * Returns the name of the column containing the node UOID (both in clustered
181      * and not clustered case).
182      */

183     public String JavaDoc getUOIDColumnName()
184     {
185         if (uoidColumn != null)
186             return uoidColumn.getColumnName();
187         else
188             return super.getUOIDColumnName();
189     }
190     /**
191      * USE ? Never updated !
192      */

193     public boolean isTerminatedBy(ElementDeclaration decl)
194     {
195         return endingElements.contains(decl);
196     }
197     
198     ////////////////////////////////////////////////////////////////////////////
199
// MappingInfo IMPLEMENTATION
200
////////////////////////////////////////////////////////////////////////////
201
// performed by CollectionMappingInfo in the Repository case
202
public String JavaDoc getOIDTableName() { return null;}
203     
204     // performed by CollectionMappingInfo in the Repository case
205
public String JavaDoc getOIDInsertStatement() { return null;}
206     
207     public String JavaDoc getSelectStatement() { return selectStatement;}
208     
209     public String JavaDoc getUpdateStatement() { return updateStatement;}
210     
211     ////////////////////////////////////////////////////////////////////////////
212
// PACKAGE PRIVATE
213
////////////////////////////////////////////////////////////////////////////
214
/**
215      * Used by the mapping loader when building element mapping.
216      * @param decl a schema ElementDeclaration.
217      */

218     void addMappedChild(ElementDeclaration decl)
219     {
220         if (!mappedElements.contains(decl))
221             mappedElements.add(decl);
222     }
223     
224     /**
225      * Used by the mapping loader to perform TableMapping initialization
226      * including column mapping check and statement building.
227      * Called after ColumnMapping registration.
228      */

229     void initialize(AbstractConnection conn) throws SAXException JavaDoc
230     {
231         initializeColumnMappings();
232         checkMapping();
233         initializeCluster();
234         initializeStatements(conn);
235         initializeEndingElements();
236     }
237     
238     boolean isInitialized() { return primaryKeyColumnMappings != null;}
239     
240     ////////////////////////////////////////////////////////////////////////////
241
// PRIVATE
242
////////////////////////////////////////////////////////////////////////////
243
/**
244      * Checks against column's metadata info got from JDBC that if a
245      * column is not registered is authorized because it's nullable
246      */

247     private void initializeColumnMappings() throws SAXException JavaDoc
248     {
249         if (getColumnMappingCount() == 0)
250             throw new SAXException JavaDoc("Table mapping must specify at least one column mapping");
251         
252         /* Set the Key columns used for user table join */
253         int uoidColumnCount = 0;
254         
255         if (keyColumnCount > 0) // A PK exists use it
256
{
257             joinMode = USE_PK;
258             uoidColumnCount = keyColumnCount;
259         }
260         else if (keyGenColumnCount > 0) // use key generators
261
{
262             joinMode = USE_KEYGEN;
263             uoidColumnCount = keyColumnCount = keyGenColumnCount;
264         }
265         else // use all columns
266
{
267             joinMode = USE_ALL;
268             uoidColumnCount = getColumnCount();
269         }
270         
271         primaryKeyColumnMappings = new ColumnMapping[keyColumnCount];
272         OIDColumnMappings = new ColumnMapping[uoidColumnCount];
273         updateColumnMappings = new ColumnMapping[updateColumnCount];
274         selectColumnMappings = new ColumnMapping[selectColumnCount];
275         fetchColumnMappings = new ColumnMapping[fetchColumnCount];
276         denseColumnMappingArray = new ColumnMappingImpl[columnMappingCount];
277         
278         int joinColumnCount = 2; // Start with 2 because of UOID and path
279
int OIDcount = 0;
280         
281         Iterator it = getColumnMappingIterator();
282         while (it.hasNext())
283         {
284             ColumnMappingImpl mapping = (ColumnMappingImpl)it.next();
285             
286             denseColumnMappingArray[mapping.getInsertColumnIndex()] = mapping;
287             
288             switch (joinMode)
289             {
290                 case USE_PK:
291                     if (mapping.getKeyColumnIndex() != -1)
292                     {
293                         primaryKeyColumnMappings[mapping.getKeyColumnIndex()] = mapping;
294                         mapping.setJoinColumnIndex(joinColumnCount++);
295                         OIDColumnMappings[mapping.getKeyColumnIndex()] = mapping;
296                     }
297                     break;
298                 case USE_KEYGEN:
299                     if (mapping.getKeyGenColumnIndex() != -1)
300                     {
301                         primaryKeyColumnMappings[mapping.getKeyGenColumnIndex()] = mapping;
302                         mapping.setJoinColumnIndex(joinColumnCount++);
303                         OIDColumnMappings[mapping.getKeyGenColumnIndex()] = mapping;
304                     }
305                     break;
306                 case USE_ALL:
307                     mapping.setJoinColumnIndex(joinColumnCount++);
308                     OIDColumnMappings[OIDcount++] = mapping;
309                     break;
310             }
311             if (mapping.getSelectColumnIndex() != -1)
312                 selectColumnMappings[mapping.getSelectColumnIndex()] = mapping;
313             
314             if (mapping.getFetchColumnIndex() != -1)
315                 fetchColumnMappings[mapping.getFetchColumnIndex()] = mapping;
316
317             if (mapping.getUpdateColumnIndex() != -1) // should be mutually exclusive with select
318
updateColumnMappings[mapping.getUpdateColumnIndex()] = mapping;
319         }
320     }
321
322     /**
323      * Checks against column's metadata info got from JDBC that if a
324      * column is not registered is authorized because it's nullable
325      */

326     private void checkMapping() throws SAXException JavaDoc
327     {
328         // for update , check or select, a select column must exist at least
329
if ((action != INSERT) && (selectColumnCount == 0))
330             throw new SAXException JavaDoc("When using the SELECT, CHECK or UPDATE mode, there must be at least one 'select' column.");
331         
332         // The following tests are not performed when generating table scripts
333
if (getMetaData() != null)
334         {
335             // for update or select : mapping can be missing for nullable columns
336
if ((action == INSERT) || (action == CHECK))
337             {
338                 Iterator it = tableMetaData.getColumnsMetaData().iterator();
339                 while (it.hasNext())
340                 {
341                     ColumnMetaData cmeta = (ColumnMetaData)it.next();
342                     ColumnMapping mapping = getColumnMapping(cmeta.getColumnName());
343
344                     // check mapping completeness
345
if (mapping == null && !cmeta.isOptional())
346                         log.warn("Missing mapping specification for " + cmeta);
347                 }
348             }
349             
350             // REPOSITORY ONLY
351
if (!userTablesOnly)
352             {
353                 if (action == INSERT)
354                 {
355                     if (joinMode != USE_PK) // PK is unique...
356
{
357                         // Check that the "key" generators are unique
358
Set keys = new HashSet();
359                         ColumnMappingImpl column;
360                         
361                         for (int i = 0; i < denseColumnMappingArray.length; i++)
362                         {
363                             column = denseColumnMappingArray[i];
364                             if (column.isInJoin())
365                                 keys.add(column.getMetaData());
366                         }
367                         
368                         // compare this set to every constraint (equals is performed on
369
// ColumnMetadata objects and not column names for performance reasons)
370
Iterator it = getMetaData().getUniqueConstraints().iterator();
371                         boolean matches = false;
372                         
373                         while (it.hasNext() && !matches)
374                             matches |= keys.containsAll((Collection)it.next());
375                         
376                         // TO IMPROVE : do not force to have a unique constraint when generator UOID is inKey
377
if (!matches)
378                             throw new SAXException JavaDoc("When using the INSERT mode, a relational UNIQUE constraint (or Primary Key) must be defined on key columns.");
379                     }
380                 }
381                 else if (action != SELECT) // update or check : key columns must be included in select columns
382
{
383                     HashSet inter = new HashSet(Arrays.asList(OIDColumnMappings));
384                     inter.retainAll(Arrays.asList(updateColumnMappings));
385                     // not clustered
386
if (!inter.isEmpty())
387                         throw new SAXException JavaDoc("When using the CHECK or UPDATE mode, Primary Key columns cannot be updated. Check that attribute 'inSelect' is true for columns " + inter);
388                     // Note: inKey generators are automatically excluded from update.
389
}
390             }
391         }
392     }
393     
394     /**
395      * Checks if the OID table is to be clustered to the values
396      */

397     private void initializeCluster() throws SAXException JavaDoc
398     {
399         ColumnMappingImpl column;
400         boolean oid = false, root = false;
401         SystemVariableGenerator sysGen;
402         for (int i = 0; i < denseColumnMappingArray.length; i++)
403         {
404             column = denseColumnMappingArray[i];
405             if (column.getGenerator() instanceof SystemVariableGenerator)
406             {
407                 sysGen = (SystemVariableGenerator)column.getGenerator();
408                 if (sysGen.getType() == PATHID_CODE) // not mandatory
409
pathColumn = column;
410                 else if (sysGen.getType() == UOID_CODE)
411                     uoidColumn = column;
412             }
413         }
414         if (userTablesOnly || ((action == INSERT)
415         && (uoidColumn != null) && (uoidColumn.getKeyGenColumnIndex() >= 0)
416         && (pathColumn != null) && (pathColumn.getKeyGenColumnIndex() >= 0)))
417             clustered = true;
418     }
419     
420     /**
421      * Build SQL strings for use in JDBC PreparedStatements/.
422      */

423     void initializeStatements(AbstractConnection conn)
424     {
425         if (tableMetaData == null)
426             return;
427         StringBuffer JavaDoc lColumnList = new StringBuffer JavaDoc(),
428             lNamedColumnList = new StringBuffer JavaDoc(),
429             lWildcards = new StringBuffer JavaDoc();
430         
431         // Basic patterns construction
432
for (int i = 0; i < denseColumnMappingArray.length; i++)
433         {
434             ColumnMappingImpl mapping = denseColumnMappingArray[i];
435             String JavaDoc columnName = mapping.getColumnName();
436             if (conn.useDoubleQuotes4DDLNames())
437                 columnName = '"' + columnName + '"';
438             if (lColumnList.length() > 0)
439             {
440                 lColumnList.append(", ");
441                 lNamedColumnList.append(", ");
442                 lWildcards.append( ", ");
443             }
444             lColumnList.append(columnName);
445             lNamedColumnList.append("w.");
446             lNamedColumnList.append(columnName);
447             lWildcards.append("?");
448             if (mapping.isInJoin())
449             {
450                 /* getting OID table columns creation info from database metadata */
451                 ColumnMetaData cmeta = mapping.getMetaData();
452                 String JavaDoc columnType = null;
453                 if (cmeta.getColumnSize() > 0)
454                     columnType = cmeta.getTypeCreationString();
455                 if (joinColumnList.length() > 0)
456                 {
457                     joinColumnList += ", ";
458                     joinWildcards += ", ";
459                     joinColumnTypes += ", ";
460                     joinCondition += " AND ";
461                 }
462                 joinColumnList += columnName;
463                 joinWildcards += "?";
464                 joinColumnTypes += columnName + " " + columnType;
465                 if (mapping.getMinOccurs() > 0) // SR 14/05/01 was 1 ! =multivaluated and not mandatory...
466
joinCondition += "v."+columnName+"=w."+columnName;
467                 else
468                     joinCondition += "((v."+columnName+"=w."+columnName+") OR (v."+columnName+" IS NULL AND w."+columnName+" IS NULL))";
469             }
470         }
471         columnList = lColumnList.toString();
472         namedColumnList = lNamedColumnList.toString();
473         wildcards = lWildcards.toString();
474         
475         lColumnList.setLength(0);
476         for (int i = 0; i < selectColumnMappings.length; i++)
477         {
478             if (i > 0)
479                 lColumnList.append(" AND ");
480             if (conn.useDoubleQuotes4DDLNames())
481                 lColumnList.append('"');
482             lColumnList.append(selectColumnMappings[i].getColumnName());
483             if (conn.useDoubleQuotes4DDLNames())
484                 lColumnList.append('"');
485             lColumnList.append(" = ?");
486         }
487         String JavaDoc selectCondition = lColumnList.toString();
488         
489         lColumnList.setLength(0);
490         for (int i = 0; i < updateColumnMappings.length; i++)
491         {
492             if (i > 0)
493                 lColumnList.append(", ");
494             if (conn.useDoubleQuotes4DDLNames())
495                 lColumnList.append('"');
496             lColumnList.append(updateColumnMappings[i].getColumnName());
497             if (conn.useDoubleQuotes4DDLNames())
498                 lColumnList.append('"');
499             lColumnList.append(" = ?");
500         }
501         String JavaDoc updateColumnList = lColumnList.toString();
502         
503         lColumnList.setLength(0);
504         for (int i = 0; i < fetchColumnMappings.length; i++)
505         {
506             if (i > 0)
507                 lColumnList.append(", ");
508             if (conn.useDoubleQuotes4DDLNames())
509                 lColumnList.append('"');
510             lColumnList.append(fetchColumnMappings[i].getColumnName());
511             if (conn.useDoubleQuotes4DDLNames())
512                 lColumnList.append('"');
513         }
514         String JavaDoc fetchColumnList = lColumnList.toString();
515         
516         // Statements construction
517
String JavaDoc tableName = getTableName(); // collection name is fake here : for default mapping
518
if (conn.useDoubleQuotes4DDLNames())
519             tableName = '"' + tableName + '"';
520         StringBuffer JavaDoc sql = new StringBuffer JavaDoc();
521         sql.append("INSERT INTO ");
522         sql.append(tableName);
523         sql.append('(');
524         sql.append(columnList);
525         sql.append(") VALUES(");
526         sql.append(wildcards);
527         sql.append(')');
528         insertStatement = sql.toString();
529         if (selectCondition.length() > 0)
530         {
531             sql.setLength(0);
532             sql.append("SELECT ");
533             if (fetchColumnCount == 0)
534                 sql.append('1');
535             else
536                 sql.append(fetchColumnList);
537             sql.append(" FROM ");
538             sql.append(tableName);
539             sql.append(" WHERE ");
540             sql.append(selectCondition);
541             selectStatement = sql.toString();
542             if (updateColumnList.length() > 0)
543             {
544                 sql.setLength(0);
545                 sql.append("UPDATE ");
546                 sql.append(tableName);
547                 sql.append(" SET ");
548                 sql.append(updateColumnList);
549                 sql.append(" WHERE ");
550                 sql.append(selectCondition);
551                 updateStatement = sql.toString();
552             }
553         }
554     }
555     
556     /* Not implemented : USE ? */
557     void initializeEndingElements() {}
558     
559     /**
560      * Called by the RepositoryMapping object once TableMappings have
561      * been constructed to build dependance link between TableMappings
562      * due to references between ColumnMapping objects.
563      */

564     Collection initializeDependencies() throws SAXException JavaDoc
565     {
566         if (dependencies == null)
567         {
568             dependencies = new HashSet();
569             for (int i = 0; i < denseColumnMappingArray.length; i++)
570             {
571                 int tableIndex = denseColumnMappingArray[i].getTableRefIndex();
572                 if (tableIndex != -1)
573                 {
574                     TableMappingImpl tm = (TableMappingImpl)colMapping.getTableMapping(tableIndex);
575                     dependencies.add(tm);
576                     Collection dep = tm.getDependencies();
577                     if (dep == null) // SR : Bug 14/11/2000 : was if (tm == null)
578
dep = tm.initializeDependencies();
579                     dependencies.addAll(dep);
580                 }
581             }
582             if (dependencies.contains(this))
583                 throw new SAXException JavaDoc("Circular integrity constraints between tables are not supported");
584         }
585         return dependencies;
586     }
587     
588     /**
589      * Used by the mapping loader to attach the ColumnMapping objects it is
590      * building to this TableMapping.
591      */

592     int register(ColumnMapping mapping) throws SAXException JavaDoc
593     {
594         // check the column hasn't got already a mapping
595
if (!userTablesOnly) // only for the repository (not the mapper)
596
{
597             Iterator it = getColumnMappingIterator();
598             ColumnMapping current = null;
599             while (it.hasNext())
600             {
601                 current = (ColumnMappingImpl)it.next();
602                 if (current.getColumnName().equalsIgnoreCase(mapping.getColumnName()))
603                     throw new SAXException JavaDoc("A column mapping has already been defined on column '"
604                     + current.getColumnName() + "' for " + current.getSchemaComponent() + ".");
605             }
606         }
607         
608         int index;
609         
610         // table DDL generation
611
if (tableMetaData == null) // used as a flag for detecting the table DDL generation
612
{
613             // resize array if necessary
614
if (getColumnMappingCount() == columnMappings.length)
615             {
616                 ColumnMapping[] newArray = new ColumnMapping[columnMappings.length * 2];
617                 System.arraycopy(columnMappings, 0, newArray, 0, columnMappings.length);
618                 columnMappings = newArray;
619             }
620             index = getColumnMappingCount();
621         }
622         else
623         {
624             // Desactivate batch for LOB types (batcher should not use batch API when size is 1)
625
ColumnMetaData cMeta = mapping.getMetaData();
626             if (cMeta.isLongType())
627                 batchSize = 1;
628             index = cMeta.getOrdinalPosition() - 1;
629         }
630         columnMappings[index] = mapping;
631         return index; // because JDBC parameters starts with 1
632
}
633     
634     int incrementColumnMappingCount()
635     {
636         return columnMappingCount++;
637     }
638
639     /**
640      * Increment the counter for the columns containing key generators.
641      * May be replaced by PK if any.
642      * Called at initialization of this object or columns
643      * that are registered to it.
644      */

645     int incrementKeyColumnCount()
646     {
647         return keyColumnCount++;
648     }
649
650     /**
651      * Increment the counter for the columns that constitute the where
652      * clause for selection. Called at initialization of columns that
653      * are registered to it.
654      */

655     int incrementSelectColumnCount()
656     {
657         return selectColumnCount++;
658     }
659     
660     /**
661      * Increment the counter for the columns that constitute the select
662      * clause for selection. Called at initialization of columns that
663      * are registered to it.
664      */

665     int incrementFetchColumnCount()
666     {
667         return fetchColumnCount++;
668     }
669     
670     /**
671      * Increment the counter for the columns that are likely to be updated
672      * in a row. Should match with the complement of select columns.
673      */

674     int incrementUpdateColumnCount()
675     {
676         return updateColumnCount++;
677     }
678     
679     int incrementKeyGenColumnCount()
680     {
681         return keyGenColumnCount++;
682     }
683     
684 }
685
Popular Tags