001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/io/datastore/sql/transaction/insert/InsertRow.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006     and
007       lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035    ----------------------------------------------------------------------------*/
036    package org.deegree.io.datastore.sql.transaction.insert;
037    
038    import java.util.ArrayList;
039    import java.util.Collection;
040    import java.util.HashMap;
041    import java.util.HashSet;
042    import java.util.Iterator;
043    import java.util.LinkedHashMap;
044    import java.util.List;
045    import java.util.Map;
046    import java.util.Set;
047    
048    import org.deegree.framework.log.ILogger;
049    import org.deegree.framework.log.LoggerFactory;
050    import org.deegree.i18n.Messages;
051    import org.deegree.io.datastore.TransactionException;
052    import org.deegree.ogcwebservices.wfs.operation.transaction.Insert;
053    
054    /**
055     * Represents a table row (columns + values) which has to be inserted as part of an {@link Insert} operation.
056     *
057     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
058     * @author last edited by: $Author: mschneider $
059     *
060     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
061     */
062    public class InsertRow {
063    
064        private static final ILogger LOG = LoggerFactory.getLogger( InsertRow.class );
065    
066        /** Database table */
067        protected String table;
068    
069        /** Maps column names to their corresponding {@link InsertField} instance. */
070        protected Map<String, InsertField> columnMap = new LinkedHashMap<String, InsertField>();
071    
072        /**
073         * Creates a new <code>InsertRow</code> instance for the given table.
074         *
075         * @param table
076         */
077        public InsertRow( String table ) {
078            this.table = table;
079        }
080    
081        /**
082         * Returns the name of table.
083         *
084         * @return the name of table
085         */
086        public String getTable() {
087            return this.table;
088        }
089    
090        /**
091         * Sets the value to be inserted in the given table column.
092         * <p>
093         * In complex + erroneous mappings (or feature instances), it may happen that different property values (mapped to
094         * the same column) imply different values. This is checked for and in case an {@link TransactionException} is
095         * thrown.
096         *
097         * @param column
098         * @param value
099         * @param sqlType
100         * @param isPK
101         * @return column + value to be set
102         * @throws TransactionException
103         *             if the value for the column clashes
104         */
105        public InsertField setColumn( String column, Object value, int sqlType, boolean isPK )
106                                throws TransactionException {
107    
108            InsertField field = new InsertField( this, column, sqlType, value, isPK );
109            InsertField presentField = columnMap.get( column );
110            if ( presentField != null && ( !presentField.getValue().toString().equals( value.toString() ) ) ) {
111                Object[] params = new Object[] { column, this.table, presentField.getValue().toString(), value.toString() };
112                String msg = Messages.getMessage( "DATASTORE_AMBIGOUS_COLUMN_VALUES", params );
113                throw new TransactionException( msg );
114            }
115            if ( presentField == null ) {
116                this.columnMap.put( column, field );
117            }
118            // TODO type check
119            return field;
120        }
121    
122        /**
123         * Sets the value to be inserted in the given geometry column.
124         * <p>
125         * In complex + erroneous mappings (or feature instances), it may happen that different property values (mapped to
126         * the same column) imply different values. This is checked for and in case an {@link TransactionException} is
127         * thrown.
128         *
129         * @param column
130         * @param value
131         * @param sqlType
132         * @param isPK
133         * @param internalSrsCode
134         * @return column + value to be set
135         * @throws TransactionException
136         *             if the value for the column clashes
137         */
138        public InsertGeometryField setGeometryColumn( String column, Object value, int sqlType, boolean isPK,
139                                                      int internalSrsCode )
140                                throws TransactionException {
141    
142            InsertGeometryField field = new InsertGeometryField( this, column, sqlType, value, isPK, internalSrsCode );
143            InsertField presentField = columnMap.get( column );
144            if ( presentField != null && ( !presentField.getValue().toString().equals( value.toString() ) ) ) {
145                Object[] params = new Object[] { column, this.table, presentField.getValue().toString(), value.toString() };
146                String msg = Messages.getMessage( "DATASTORE_AMBIGOUS_COLUMN_VALUES", params );
147                throw new TransactionException( msg );
148            }
149            if ( presentField == null ) {
150                this.columnMap.put( column, field );
151            }
152            // TODO type check
153            return field;
154        }
155    
156        /**
157         * Sets the value to be inserted in the given table column - the column references the given {@link InsertField} and
158         * thus must have the same value as the referenced column.
159         * <p>
160         * In complex + erroneous mappings (or feature instances), it may happen that different property values (mapped to
161         * the same column) imply different values. This is checked for and in case an {@link TransactionException} is
162         * thrown.
163         *
164         * @param column
165         * @param referencedField
166         * @throws TransactionException
167         *             if the value for the column clashes
168         */
169        public void linkColumn( String column, InsertField referencedField )
170                                throws TransactionException {
171    
172            if ( referencedField.getValue() == null ) {
173                LOG.logError( "linkColumn (): referencedField is null" );
174                throw new TransactionException( "linkColumn (): referenced field is null!" );
175            }
176    
177            InsertField presentField = this.columnMap.get( column );
178            if ( presentField != null
179                 && ( !presentField.getValue().toString().equals( referencedField.getValue().toString() ) ) ) {
180                Object[] params = new Object[] { column, this.table, presentField.getValue().toString(),
181                                                referencedField.getValue().toString() };
182                String msg = Messages.getMessage( "DATASTORE_AMBIGOUS_COLUMN_VALUES", params );
183                throw new TransactionException( msg );
184            }
185    
186            InsertField field = presentField;
187            if ( presentField != null ) {
188                presentField.relinkField( referencedField );
189            } else {
190                field = new InsertField( this, column, referencedField );
191                this.columnMap.put( column, field );
192            }
193            referencedField.addReferencingField( field );
194        }
195    
196        /**
197         * Returns all {@link InsertField}s.
198         *
199         * @return all InsertFields
200         */
201        public Collection<InsertField> getColumns() {
202            return this.columnMap.values();
203        }
204    
205        /**
206         * Returns the {@link InsertField} for the given column name.
207         *
208         * @param column
209         *            requested column name
210         * @return the InsertField for the given column name, or null if it does not exist
211         */
212        public InsertField getColumn( String column ) {
213            return this.columnMap.get( column );
214        }
215    
216        /**
217         * Returns the {@link InsertField} for the primary key column.
218         *
219         * @return the InsertField for the primary key column
220         */
221        public InsertField getPKColumn() {
222            InsertField pkField = null;
223            for ( InsertField field : this.columnMap.values() ) {
224                if ( field.isPK() ) {
225                    pkField = field;
226                    break;
227                }
228            }
229            return pkField;
230        }
231    
232        /**
233         * Returns all {@link InsertField} that reference a column in this <code>InsertRow</code>. The fields may well be
234         * from other tables.
235         *
236         * @return all InsertFields that reference a column in this InsertRow
237         */
238        public List<InsertField> getReferencingFields() {
239            List<InsertField> referencingFields = new ArrayList<InsertField>();
240            Iterator<InsertField> iter = this.columnMap.values().iterator();
241            while ( iter.hasNext() ) {
242                InsertField field = iter.next();
243                referencingFields.addAll( field.getReferencingFields() );
244            }
245            return referencingFields;
246        }
247    
248        /**
249         * Returns all {@link InsertRow}s that reference a column in this <code>InsertRow</code>. The rows may well be from
250         * other tables.
251         *
252         * @return all InsertRows that reference a column in this InsertRow
253         */
254        Collection<InsertRow> getReferencingRows() {
255            Set<InsertRow> referencingRows = new HashSet<InsertRow>();
256            Iterator<InsertField> iter = this.columnMap.values().iterator();
257            while ( iter.hasNext() ) {
258                InsertField field = iter.next();
259                referencingRows.addAll( field.getReferencingRows() );
260            }
261            return referencingRows;
262        }
263    
264        /**
265         * Returns all {@link InsertField}s that are referenced by a field from this <code>InsertRow</code>. The fields may
266         * well be from other tables.
267         *
268         * @return all InsertField that are referenced by a field from this InsertRow
269         */
270        List<InsertField> getReferencedFields() {
271            List<InsertField> referencedFields = new ArrayList<InsertField>();
272            Iterator<InsertField> iter = this.columnMap.values().iterator();
273            while ( iter.hasNext() ) {
274                InsertField field = iter.next();
275                InsertField referencedField = field.getReferencedField();
276                if ( referencedField != null ) {
277                    referencedFields.add( referencedField );
278                }
279            }
280            return referencedFields;
281        }
282    
283        /**
284         * Returns all {@link InsertRow}s that are referenced by a field from this <code>InsertRow</code>. The rows may well
285         * be from other tables.
286         *
287         * @return all InsertRows that are referenced by a field from this InsertRow
288         */
289        private Collection<InsertRow> getReferencedRows() {
290            Set<InsertRow> referencedRows = new HashSet<InsertRow>();
291            Iterator<InsertField> iter = this.columnMap.values().iterator();
292            while ( iter.hasNext() ) {
293                InsertField field = iter.next();
294                InsertRow referencedRow = field.getReferencedRow();
295                if ( referencedRow != null ) {
296                    referencedRows.add( referencedRow );
297                }
298            }
299            return referencedRows;
300        }
301    
302        /**
303         * Sorts the given <code>InsertRow</code>s topologically (respecting the foreign key constraints), so they can be
304         * inserted in the resulting order without causing foreign key violations.
305         * <p>
306         * Number of precedessors (pre): number of fields that *are referenced by* this row Number of successors (post) :
307         * number of fields that *reference* this row
308         *
309         * @param inserts
310         *            insert rows to sort
311         * @return the nodes of the graph in topological order if no cycle is present, else in arbitrary order
312         */
313        public static List<InsertRow> getInsertOrder( List<InsertRow> inserts ) {
314    
315            List<InsertRow> result = new ArrayList<InsertRow>();
316    
317            // key: inserts, value: number of fields that are referenced by this row
318            Map<InsertRow, Integer> preMap = new HashMap<InsertRow, Integer>();
319    
320            // key: inserts with no foreign key constraints
321            List<InsertRow> noPre = new ArrayList<InsertRow>();
322    
323            // build map
324            Iterator<InsertRow> insertIter = inserts.iterator();
325            while ( insertIter.hasNext() ) {
326                InsertRow insertRow = insertIter.next();
327                int pre = insertRow.getReferencedFields().size();
328                LOG.logDebug( "Adding row to preMap: " + insertRow );
329                preMap.put( insertRow, pre );
330                if ( pre == 0 ) {
331                    noPre.add( insertRow );
332                }
333            }
334    
335            while ( !noPre.isEmpty() ) {
336                // select an insert row that has no open fk constraints
337                InsertRow insertRow = noPre.get( 0 );
338                noPre.remove( 0 );
339                result.add( insertRow );
340    
341                // decrease the number of pending fk constraints for all insert rows that
342                // reference the currently processed insert row
343                Collection<InsertField> postList = insertRow.getReferencingFields();
344                Iterator<InsertField> iter = postList.iterator();
345                while ( iter.hasNext() ) {
346                    InsertField postField = iter.next();
347                    if ( preMap.get( postField.getRow() ) == null ) {
348                        LOG.logDebug( "No pre info for: " + postField.getRow() );
349                    }
350                    int pre = preMap.get( postField.getRow() );
351                    preMap.put( postField.getRow(), --pre );
352                    if ( pre == 0 ) {
353                        noPre.add( postField.getRow() );
354                    }
355                }
356            }
357    
358            if ( result.size() != inserts.size() ) {
359                Collection<InsertRow> cycle = InsertRow.findCycle( inserts );
360                Iterator<InsertRow> cycleIter = cycle.iterator();
361                StringBuffer sb = new StringBuffer();
362                while ( cycleIter.hasNext() ) {
363                    sb.append( cycleIter.next() );
364                    if ( cycle.iterator().hasNext() ) {
365                        sb.append( " -> " );
366                    }
367                }
368                String msg = Messages.getMessage( "DATASTORE_FK_CYCLE", sb.toString() );
369                LOG.logWarning( msg );
370                result.clear();
371                result.addAll( inserts );
372            }
373            return result;
374        }
375    
376        /**
377         * Checks if the given <code>InsertRow</code>s contain cyclic fk constraints.
378         *
379         * @param rows
380         * @return steps of the cycle, or null if there is none
381         */
382        private static Collection<InsertRow> findCycle( Collection<InsertRow> rows ) {
383    
384            Iterator<InsertRow> rowsIter = rows.iterator();
385            while ( rowsIter.hasNext() ) {
386                InsertRow begin = rowsIter.next();
387                Iterator<InsertRow> refIter = begin.getReferencedRows().iterator();
388                while ( refIter.hasNext() ) {
389                    InsertRow referencedRow = refIter.next();
390                    List<InsertRow> cycle = findCycleRecursion( referencedRow, begin, new ArrayList<InsertRow>() );
391                    if ( cycle != null ) {
392                        return cycle;
393                    }
394                }
395            }
396            return null;
397        }
398    
399        private static List<InsertRow> findCycleRecursion( InsertRow next, InsertRow begin, List<InsertRow> steps ) {
400    
401            if ( steps.contains( next ) ) {
402                steps.add( next );
403                return steps;
404            }
405            steps.add( next );
406    
407            Iterator<InsertRow> refIter = next.getReferencedRows().iterator();
408            while ( refIter.hasNext() ) {
409                InsertRow referencedRow = refIter.next();
410                List<InsertRow> cycle = findCycleRecursion( referencedRow, begin, steps );
411                if ( cycle != null ) {
412                    return cycle;
413                }
414            }
415            steps.remove( steps.size() - 1 );
416            return null;
417        }
418    
419        /**
420         * Returns a string representation of the object.
421         *
422         * @return a string representation of the object
423         */
424        @Override
425        public String toString() {
426            StringBuffer sb = new StringBuffer( "InsertRow, table: '" );
427            sb.append( this.table );
428            sb.append( "'" );
429            Iterator<String> columnIter = this.columnMap.keySet().iterator();
430            while ( columnIter.hasNext() ) {
431                sb.append( ", " );
432                String column = columnIter.next();
433                InsertField field = this.columnMap.get( column );
434                sb.append( field );
435            }
436            return sb.toString();
437        }
438    }