001    package org.apache.ojb.broker.core;
002    
003    /* Copyright 2003-2005 The Apache Software Foundation
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.sql.SQLException;
023    
024    import org.apache.commons.lang.builder.EqualsBuilder;
025    import org.apache.commons.lang.builder.HashCodeBuilder;
026    import org.apache.commons.lang.builder.ToStringBuilder;
027    import org.apache.commons.lang.ArrayUtils;
028    import org.apache.ojb.broker.MtoNImplementor;
029    import org.apache.ojb.broker.OJBRuntimeException;
030    import org.apache.ojb.broker.PersistenceBrokerException;
031    import org.apache.ojb.broker.PersistenceBrokerSQLException;
032    import org.apache.ojb.broker.accesslayer.ResultSetAndStatement;
033    import org.apache.ojb.broker.core.proxy.ProxyHelper;
034    import org.apache.ojb.broker.metadata.ClassDescriptor;
035    import org.apache.ojb.broker.metadata.CollectionDescriptor;
036    import org.apache.ojb.broker.metadata.DescriptorRepository;
037    import org.apache.ojb.broker.metadata.FieldDescriptor;
038    import org.apache.ojb.broker.metadata.JdbcType;
039    import org.apache.ojb.broker.query.Query;
040    import org.apache.ojb.broker.util.logging.Logger;
041    import org.apache.ojb.broker.util.logging.LoggerFactory;
042    
043    /**
044     * Manage all stuff related to non-decomposed M:N association.
045     *
046     * @author <a href="mailto:thma@apache.org">Thomas Mahler<a>
047     * @author <a href="mailto:leandro@ibnetwork.com.br">Leandro Rodrigo Saad Cruz<a>
048     * @author <a href="mailto:mattbaird@yahoo.com">Matthew Baird<a>
049     * @author <a href="mailto:jbraeuchi@hotmail.com">Jakob Braeuchi</a>
050     * @author <a href="mailto:armin@codeAuLait.de">Armin Waibel</a>
051     * @version $Id: MtoNBroker.java,v 1.1 2007-08-24 22:17:35 ewestfal Exp $
052     */
053    public class MtoNBroker
054    {
055        private Logger log = LoggerFactory.getLogger(MtoNBroker.class);
056    
057        private PersistenceBrokerImpl pb;
058        /**
059         * Used to store {@link GenericObject} while transaction running, used as
060         * workaround for m:n insert problem.
061         * TODO: find better solution for m:n handling
062         */
063        private List tempObjects = new ArrayList();
064    
065        public MtoNBroker(final PersistenceBrokerImpl broker)
066        {
067            this.pb = broker;
068        }
069    
070        public void reset()
071        {
072            tempObjects.clear();
073        }
074    
075        /**
076         * Stores new values of a M:N association in a indirection table.
077         *
078         * @param cod        The {@link org.apache.ojb.broker.metadata.CollectionDescriptor} for the m:n relation
079         * @param realObject The real object
080         * @param otherObj   The referenced object
081         * @param mnKeys     The all {@link org.apache.ojb.broker.core.MtoNBroker.Key} matching the real object
082         */
083        public void storeMtoNImplementor(CollectionDescriptor cod, Object realObject, Object otherObj, Collection mnKeys)
084        {
085            ClassDescriptor cld = pb.getDescriptorRepository().getDescriptorFor(realObject.getClass());
086            ValueContainer[] pkValues = pb.serviceBrokerHelper().getKeyValues(cld, realObject);
087            String[] pkColumns = cod.getFksToThisClass();
088    
089            ClassDescriptor otherCld = pb.getDescriptorRepository().getDescriptorFor(ProxyHelper.getRealClass(otherObj));
090            ValueContainer[] otherPkValues = pb.serviceBrokerHelper().getKeyValues(otherCld, otherObj);
091    
092            String[] otherPkColumns = cod.getFksToItemClass();
093            String table = cod.getIndirectionTable();
094            MtoNBroker.Key key = new MtoNBroker.Key(otherPkValues);
095    
096            if(mnKeys.contains(key))
097            {
098                return;
099            }
100    
101            /*
102            fix for OJB-76, composite M & N keys that have some fields common
103            find the "shared" indirection table columns, values and remove these from m- or n- side
104            */
105            for(int i = 0; i < otherPkColumns.length; i++)
106            {
107                int index = ArrayUtils.indexOf(pkColumns, otherPkColumns[i]);
108                if(index != -1)
109                {
110                    // shared indirection table column found, remove this column from one side
111                    pkColumns = (String[]) ArrayUtils.remove(pkColumns, index);
112                    // remove duplicate value too
113                    pkValues = (ValueContainer[]) ArrayUtils.remove(pkValues, index);
114                }
115            }
116    
117            String[] cols = mergeColumns(pkColumns, otherPkColumns);
118            String insertStmt = pb.serviceSqlGenerator().getInsertMNStatement(table, pkColumns, otherPkColumns);
119            ValueContainer[] values = mergeContainer(pkValues, otherPkValues);
120            GenericObject gObj = new GenericObject(table, cols, values);
121            if(! tempObjects.contains(gObj))
122            {
123                pb.serviceJdbcAccess().executeUpdateSQL(insertStmt, cld, pkValues, otherPkValues);
124                tempObjects.add(gObj);
125            }
126        }
127    
128        /**
129         * get a Collection of Keys of already existing m:n rows
130         *
131         * @param cod
132         * @param obj
133         * @return Collection of Key
134         */
135        public List getMtoNImplementor(CollectionDescriptor cod, Object obj)
136        {
137            ResultSetAndStatement rs = null;
138            ArrayList result = new ArrayList();
139            ClassDescriptor cld = pb.getDescriptorRepository().getDescriptorFor(obj.getClass());
140            ValueContainer[] pkValues = pb.serviceBrokerHelper().getKeyValues(cld, obj);
141            String[] pkColumns = cod.getFksToThisClass();
142            String[] fkColumns = cod.getFksToItemClass();
143            String table = cod.getIndirectionTable();
144    
145            String selectStmt = pb.serviceSqlGenerator().getSelectMNStatement(table, fkColumns, pkColumns);
146    
147            ClassDescriptor itemCLD = pb.getDescriptorRepository().getDescriptorFor(cod.getItemClass());
148            Collection extents = pb.getDescriptorRepository().getAllConcreteSubclassDescriptors(itemCLD);
149            if(extents.size() > 0)
150            {
151                itemCLD = (ClassDescriptor) extents.iterator().next();
152            }
153            FieldDescriptor[] itemClassPKFields = itemCLD.getPkFields();
154            if(itemClassPKFields.length != fkColumns.length)
155            {
156                throw new PersistenceBrokerException("All pk fields of the element-class need to" +
157                        " be declared in the indirection table. Element class is "
158                        + itemCLD.getClassNameOfObject() + " with " + itemClassPKFields.length + " pk-fields." +
159                        " Declared 'fk-pointing-to-element-class' elements in collection-descriptor are"
160                        + fkColumns.length);
161            }
162            try
163            {
164                rs = pb.serviceJdbcAccess().executeSQL(selectStmt, cld, pkValues, Query.NOT_SCROLLABLE);
165                while(rs.m_rs.next())
166                {
167                    ValueContainer[] row = new ValueContainer[fkColumns.length];
168                    for(int i = 0; i < row.length; i++)
169                    {
170                        row[i] = new ValueContainer(rs.m_rs.getObject(i + 1), itemClassPKFields[i].getJdbcType());
171                    }
172                    result.add(new MtoNBroker.Key(row));
173                }
174            }
175            catch(PersistenceBrokerException e)
176            {
177                throw e;
178            }
179            catch(SQLException e)
180            {
181                throw new PersistenceBrokerSQLException(e);
182            }
183            finally
184            {
185                if(rs != null) rs.close();
186            }
187            return result;
188        }
189    
190        /**
191         * delete all rows from m:n table belonging to obj
192         *
193         * @param cod
194         * @param obj
195         */
196        public void deleteMtoNImplementor(CollectionDescriptor cod, Object obj)
197        {
198            ClassDescriptor cld = pb.getDescriptorRepository().getDescriptorFor(obj.getClass());
199            ValueContainer[] pkValues = pb.serviceBrokerHelper().getKeyValues(cld, obj);
200            String[] pkColumns = cod.getFksToThisClass();
201            String table = cod.getIndirectionTable();
202            String deleteStmt = pb.serviceSqlGenerator().getDeleteMNStatement(table, pkColumns, null);
203            pb.serviceJdbcAccess().executeUpdateSQL(deleteStmt, cld, pkValues, null);
204        }
205    
206        /**
207         * deletes all rows from m:n table that are not used in relatedObjects
208         *
209         * @param cod
210         * @param obj
211         * @param collectionIterator
212         * @param mnKeys
213         */
214        public void deleteMtoNImplementor(CollectionDescriptor cod, Object obj, Iterator collectionIterator, Collection mnKeys)
215        {
216            if(mnKeys.isEmpty() || collectionIterator == null)
217            {
218                return;
219            }
220            List workList = new ArrayList(mnKeys);
221            MtoNBroker.Key relatedObjKeys;
222            ClassDescriptor relatedCld = pb.getDescriptorRepository().getDescriptorFor(cod.getItemClass());
223            Object relatedObj;
224    
225            // remove keys of relatedObject from the existing m:n rows in workList
226            while(collectionIterator.hasNext())
227            {
228                relatedObj = collectionIterator.next();
229                relatedObjKeys = new MtoNBroker.Key(pb.serviceBrokerHelper().getKeyValues(relatedCld, relatedObj, true));
230                workList.remove(relatedObjKeys);
231            }
232    
233            // delete all remaining keys in workList
234            ClassDescriptor cld = pb.getDescriptorRepository().getDescriptorFor(obj.getClass());
235            ValueContainer[] pkValues = pb.serviceBrokerHelper().getKeyValues(cld, obj);
236    
237            String[] pkColumns = cod.getFksToThisClass();
238            String[] fkColumns = cod.getFksToItemClass();
239            String table = cod.getIndirectionTable();
240            String deleteStmt;
241    
242            ValueContainer[] fkValues;
243            Iterator iter = workList.iterator();
244            while(iter.hasNext())
245            {
246                fkValues = ((MtoNBroker.Key) iter.next()).m_containers;
247                deleteStmt = pb.serviceSqlGenerator().getDeleteMNStatement(table, pkColumns, fkColumns);
248                pb.serviceJdbcAccess().executeUpdateSQL(deleteStmt, cld, pkValues, fkValues);
249            }
250        }
251    
252        /**
253         * @param m2n
254         */
255        public void storeMtoNImplementor(MtoNImplementor m2n)
256        {
257            if(log.isDebugEnabled()) log.debug("Storing M2N implementor [" + m2n + "]");
258            insertOrDeleteMtoNImplementor(m2n, true);
259        }
260    
261        /**
262         * @param m2n
263         */
264        public void deleteMtoNImplementor(MtoNImplementor m2n)
265        {
266            if(log.isDebugEnabled()) log.debug("Deleting M2N implementor [" + m2n + "]");
267            insertOrDeleteMtoNImplementor(m2n, false);
268        }
269    
270    
271        /**
272         * @see org.apache.ojb.broker.PersistenceBroker#deleteMtoNImplementor
273         */
274        private void insertOrDeleteMtoNImplementor(MtoNImplementor m2nImpl, boolean insert)
275                throws PersistenceBrokerException
276        {
277            //look for a collection descriptor on left  such as left.element-class-ref='right'
278            DescriptorRepository dr = pb.getDescriptorRepository();
279    
280            Object leftObject = m2nImpl.getLeftObject();
281            Class leftClass = m2nImpl.getLeftClass();
282            Object rightObject = m2nImpl.getRightObject();
283            Class rightClass = m2nImpl.getRightClass();
284    
285            //are written per class, maybe referencing abstract classes or interfaces
286            //so let's look for collection descriptors on the left class and try to
287            // handle extents on teh right class
288            ClassDescriptor leftCld = dr.getDescriptorFor(leftClass);
289            ClassDescriptor rightCld = dr.getDescriptorFor(rightClass);
290            //Vector leftColds = leftCld.getCollectionDescriptors();
291            CollectionDescriptor wanted = m2nImpl.getLeftDescriptor();
292    
293            if(leftObject == null || rightObject == null)
294            {
295                //TODO: to be implemented, must change MtoNImplementor
296                //deleteMtoNImplementor(wanted,leftObject) || deleteMtoNImplementor(wanted,rightObject)
297                log.error("Can't handle MtoNImplementor in correct way, found a 'null' object");
298            }
299            else
300            {
301                //delete only one row
302                ValueContainer[] leftPkValues = pb.serviceBrokerHelper().getKeyValues(leftCld, leftObject);
303                ValueContainer[] rightPkValues = pb.serviceBrokerHelper().getKeyValues(rightCld, rightObject);
304                String[] pkLeftColumns = wanted.getFksToThisClass();
305                String[] pkRightColumns = wanted.getFksToItemClass();
306                String table = wanted.getIndirectionTable();
307                if(table == null) throw new PersistenceBrokerException("Can't remove MtoN implementor without an indirection table");
308    
309                String stmt;
310                String[] cols = mergeColumns(pkLeftColumns, pkRightColumns);
311                ValueContainer[] values = mergeContainer(leftPkValues, rightPkValues);
312                if(insert)
313                {
314                    stmt = pb.serviceSqlGenerator().getInsertMNStatement(table, pkLeftColumns, pkRightColumns);
315                    GenericObject gObj = new GenericObject(table, cols, values);
316                    if(!tempObjects.contains(gObj))
317                    {
318                        pb.serviceJdbcAccess().executeUpdateSQL(stmt, leftCld, leftPkValues, rightPkValues);
319                        tempObjects.add(gObj);
320                    }
321                }
322                else
323                {
324                    stmt = pb.serviceSqlGenerator().getDeleteMNStatement(table, pkLeftColumns, pkRightColumns);
325                    pb.serviceJdbcAccess().executeUpdateSQL(stmt, leftCld, leftPkValues, rightPkValues);
326                }
327            }
328        }
329    
330        private String[] mergeColumns(String[] first, String[] second)
331        {
332            String[] cols = new String[first.length + second.length];
333            System.arraycopy(first, 0, cols, 0, first.length);
334            System.arraycopy(second, 0, cols, first.length, second.length);
335            return cols;
336        }
337    
338        private ValueContainer[] mergeContainer(ValueContainer[] first, ValueContainer[] second)
339        {
340            ValueContainer[] values = new ValueContainer[first.length + second.length];
341            System.arraycopy(first, 0, values, 0, first.length);
342            System.arraycopy(second, 0, values, first.length, second.length);
343            return values;
344        }
345    
346    
347    
348    // ************************************************************************
349    // inner class
350    // ************************************************************************
351    
352        /**
353         * This is a helper class to model a Key of an Object
354         */
355        private static final class Key
356        {
357            final ValueContainer[] m_containers;
358    
359            Key(final ValueContainer[] containers)
360            {
361                m_containers = new ValueContainer[containers.length];
362    
363                for(int i = 0; i < containers.length; i++)
364                {
365                    Object value = containers[i].getValue();
366                    JdbcType type = containers[i].getJdbcType();
367    
368                    // BRJ:
369                    // convert all Numbers to Long to simplify equals
370                    // Long(100) is not equal to Integer(100)
371                    //
372                    // could lead to problems when Floats are used as key
373                    // converting to String could be a better alternative
374                    if(value instanceof Number)
375                    {
376                        value = new Long(((Number) value).longValue());
377                    }
378    
379                    m_containers[i] = new ValueContainer(value, type);
380                }
381            }
382    
383            public boolean equals(Object other)
384            {
385                if(other == this)
386                {
387                    return true;
388                }
389                if(!(other instanceof Key))
390                {
391                    return false;
392                }
393    
394                Key otherKey = (Key) other;
395                EqualsBuilder eb = new EqualsBuilder();
396    
397                eb.append(m_containers, otherKey.m_containers);
398                return eb.isEquals();
399            }
400    
401            public int hashCode()
402            {
403                HashCodeBuilder hb = new HashCodeBuilder();
404                hb.append(m_containers);
405    
406                return hb.toHashCode();
407            }
408        }
409    
410    
411    
412        // ************************************************************************
413        // inner class
414        // ************************************************************************
415        private static final class GenericObject
416        {
417             private String tablename;
418            private String[] columnNames;
419            private ValueContainer[] values;
420    
421            public GenericObject(String tablename, String[] columnNames, ValueContainer[] values)
422            {
423                this.tablename = tablename;
424                this.columnNames = columnNames;
425                this.values = values;
426                if(values != null && columnNames.length != values.length)
427                {
428                    throw new OJBRuntimeException("Column name array and value array have NOT same length");
429                }
430            }
431    
432            public boolean equals(Object obj)
433            {
434                if(this == obj)
435                {
436                    return true;
437                }
438                boolean result = false;
439                if(obj instanceof GenericObject)
440                {
441                    GenericObject other = (GenericObject) obj;
442                    result = (tablename.equalsIgnoreCase(other.tablename)
443                            && (columnNames != null)
444                            && (other.columnNames != null)
445                            && (columnNames.length == other.columnNames.length));
446    
447                    if(result)
448                    {
449                        for (int i = 0; i < columnNames.length; i++)
450                        {
451                            int otherIndex = other.indexForColumn(columnNames[i]);
452                            if(otherIndex < 0)
453                            {
454                                result = false;
455                                break;
456                            }
457                            result = values[i].equals(other.values[otherIndex]);
458                            if(!result) break;
459                        }
460                    }
461                }
462                return result;
463            }
464    
465            int indexForColumn(String name)
466            {
467                int result = -1;
468                for (int i = 0; i < columnNames.length; i++)
469                {
470                    if(columnNames[i].equals(name))
471                    {
472                        result = i;
473                        break;
474                    }
475                }
476                return result;
477            }
478    
479            public int hashCode()
480            {
481                return super.hashCode();
482            }
483    
484            public ValueContainer getValueFor(String columnName)
485            {
486                try
487                {
488                    return values[indexForColumn(columnName)];
489                }
490                catch(Exception e)
491                {
492                    throw new OJBRuntimeException("Can't find value for column " + columnName
493                            + (indexForColumn(columnName) < 0 ? ". Column name was not found" : ""), e);
494                }
495            }
496    
497            public String getTablename()
498            {
499                return tablename;
500            }
501    
502            public String[] getColumnNames()
503            {
504                return columnNames;
505            }
506    
507            public ValueContainer[] getValues()
508            {
509                return values;
510            }
511    
512            public void setValues(ValueContainer[] values)
513            {
514                this.values = values;
515            }
516    
517            public String toString()
518            {
519                return new ToStringBuilder(this)
520                        .append("tableName", tablename)
521                        .append("columnNames", columnNames)
522                        .append("values", values)
523                        .toString();
524            }
525        }
526    }