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 }