| 1 | /** |
| 2 | * Copyright (C) 2006 NetMind Consulting Bt. |
| 3 | * |
| 4 | * This library is free software; you can redistribute it and/or |
| 5 | * modify it under the terms of the GNU Lesser General Public |
| 6 | * License as published by the Free Software Foundation; either |
| 7 | * version 3 of the License, or (at your option) any later version. |
| 8 | * |
| 9 | * This library is distributed in the hope that it will be useful, |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 12 | * Lesser General Public License for more details. |
| 13 | * |
| 14 | * You should have received a copy of the GNU Lesser General Public |
| 15 | * License along with this library; if not, write to the Free Software |
| 16 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 17 | */ |
| 18 | |
| 19 | package hu.netmind.beankeeper.model.impl; |
| 20 | |
| 21 | import java.util.Map; |
| 22 | import java.util.HashMap; |
| 23 | import java.util.List; |
| 24 | import java.util.Iterator; |
| 25 | import java.util.Date; |
| 26 | import java.util.ArrayList; |
| 27 | import java.util.HashSet; |
| 28 | import java.util.Stack; |
| 29 | import java.lang.reflect.Modifier; |
| 30 | import hu.netmind.beankeeper.parser.QueryStatement; |
| 31 | import hu.netmind.beankeeper.event.EventDispatcher; |
| 32 | import hu.netmind.beankeeper.event.PersistenceEventListener; |
| 33 | import hu.netmind.beankeeper.event.PersistenceEvent; |
| 34 | import hu.netmind.beankeeper.transaction.event.TransactionEvent; |
| 35 | import hu.netmind.beankeeper.transaction.event.TransactionCommittedEvent; |
| 36 | import hu.netmind.beankeeper.transaction.event.TransactionRolledbackEvent; |
| 37 | import hu.netmind.beankeeper.node.event.NodeStateChangeEvent; |
| 38 | import hu.netmind.beankeeper.common.StoreException; |
| 39 | import hu.netmind.beankeeper.type.TypeHandler; |
| 40 | import hu.netmind.beankeeper.model.*; |
| 41 | import hu.netmind.beankeeper.transaction.Transaction; |
| 42 | import hu.netmind.beankeeper.transaction.InternalTransactionTracker; |
| 43 | import hu.netmind.beankeeper.db.Database; |
| 44 | import hu.netmind.beankeeper.db.Limits; |
| 45 | import hu.netmind.beankeeper.db.SearchResult; |
| 46 | import hu.netmind.beankeeper.object.Identifier; |
| 47 | import hu.netmind.beankeeper.node.NodeManager; |
| 48 | import hu.netmind.beankeeper.type.TypeHandlerTracker; |
| 49 | import org.apache.log4j.Logger; |
| 50 | |
| 51 | /** |
| 52 | * This class keeps track of different classes and objects. It's main |
| 53 | * purpose is to implement object reflection based logic, and provide |
| 54 | * type information. |
| 55 | * @author Brautigam Robert |
| 56 | * @version Revision: $Revision$ |
| 57 | */ |
| 58 | public class ClassTrackerImpl implements ClassTracker, PersistenceEventListener |
| 59 | { |
| 60 | private static Logger logger = Logger.getLogger(ClassTrackerImpl.class); |
| 61 | private static final long ALLOCATE_COUNT = 1000; |
| 62 | private static final long MAX_OBJECT_COUNT = Long.MAX_VALUE>>16; |
| 63 | |
| 64 | private Map currentIds = new HashMap(); // Entry -> Current object Id |
| 65 | |
| 66 | private Map classInfos; // Entry->Info mapping (locally managed) |
| 67 | private Map relatedClassEntries; // Entry->Entry list (locally managed) |
| 68 | private Map rootClassEntries; // Entry->Entry list (locally managed) |
| 69 | private Map entriesById; // Id->Entry (synced) |
| 70 | private Map maxIdByEntry; // Entry->Max Id (synced) |
| 71 | private Map classIdsByEntry; // Entry->Id (synced) |
| 72 | |
| 73 | private int classMaxId = 0; |
| 74 | |
| 75 | private EventDispatcher eventDispatcher = null; // Injected |
| 76 | private InternalTransactionTracker transactionTracker = null; // Injected |
| 77 | private TypeHandlerTracker typeHandlerTracker = null; // Injected |
| 78 | private Database database = null; // Injected |
| 79 | private NodeManager nodeManager = null; // Injected |
| 80 | |
| 81 | public void init(Map parameters) |
| 82 | { |
| 83 | eventDispatcher.registerListener(this); |
| 84 | // Init local stuff |
| 85 | classInfos = new HashMap(); |
| 86 | relatedClassEntries = new HashMap(); |
| 87 | rootClassEntries = new HashMap(); |
| 88 | // Load everything |
| 89 | reload(); |
| 90 | } |
| 91 | |
| 92 | public void release() |
| 93 | { |
| 94 | eventDispatcher.unregisterListener(this); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Return whether the given class is a primitive type. |
| 99 | */ |
| 100 | private boolean isPrimitive(Class clazz) |
| 101 | { |
| 102 | if ( clazz == null ) |
| 103 | return false; |
| 104 | return (clazz.equals(int.class)) || |
| 105 | (clazz.equals(short.class)) || |
| 106 | (clazz.equals(byte.class)) || |
| 107 | (clazz.equals(long.class)) || |
| 108 | (clazz.equals(float.class)) || |
| 109 | (clazz.equals(double.class)) || |
| 110 | (clazz.equals(char.class)) || |
| 111 | (clazz.equals(boolean.class)) || |
| 112 | (clazz.equals(Integer.class)) || |
| 113 | (clazz.equals(Short.class)) || |
| 114 | (clazz.equals(Byte.class)) || |
| 115 | (clazz.equals(Long.class)) || |
| 116 | (clazz.equals(Float.class)) || |
| 117 | (clazz.equals(Double.class)) || |
| 118 | (clazz.equals(Character.class)) || |
| 119 | (clazz.equals(String.class)) || (clazz.equals(Boolean.class)) || |
| 120 | (clazz.equals(byte[].class)) || |
| 121 | (clazz.equals(Date.class)) || clazz.equals(Character.class); |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Get the type id of given class. |
| 126 | */ |
| 127 | public ClassType getType(Class clazz) |
| 128 | { |
| 129 | if ( clazz == null ) |
| 130 | return ClassType.TYPE_NONE; |
| 131 | if ( typeHandlerTracker.isHandled(clazz) ) |
| 132 | return ClassType.TYPE_HANDLED; |
| 133 | if ( isPrimitive(clazz) ) |
| 134 | return ClassType.TYPE_PRIMITIVE; |
| 135 | // Array types not allowed |
| 136 | if ( clazz.getName().startsWith("[") ) |
| 137 | return ClassType.TYPE_RESERVED; |
| 138 | // The rest is custom objects. |
| 139 | return ClassType.TYPE_OBJECT; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Get class info for a class entry. |
| 144 | */ |
| 145 | public ClassInfo getClassInfo(Class clazz, String dynamicName) |
| 146 | { |
| 147 | if ( clazz == null ) |
| 148 | return null; |
| 149 | return getClassInfo(new ClassEntry(clazz,dynamicName)); |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Get class info for an object and it's class. |
| 154 | */ |
| 155 | public ClassInfo getClassInfo(Class clazz, Object obj) |
| 156 | { |
| 157 | if ( (obj instanceof DynamicObject) && (obj.getClass().equals(clazz)) ) |
| 158 | return getClassInfo(clazz, ((DynamicObject) obj).getPersistenceDynamicName()); |
| 159 | else |
| 160 | return getClassInfo(clazz, null); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Get class info from a string. |
| 165 | */ |
| 166 | public ClassInfo getClassInfo(String className, String dynamicName) |
| 167 | { |
| 168 | if ( className == null ) |
| 169 | return null; |
| 170 | try |
| 171 | { |
| 172 | Class clazz = Class.forName(className); |
| 173 | return getClassInfo(clazz, dynamicName); |
| 174 | } catch ( StoreException e ) { |
| 175 | throw e; |
| 176 | } catch ( Exception e ) { |
| 177 | throw new StoreException("class cannot be located with name '"+className+"'",e); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Get all related classes to the given entry. Related class entries are |
| 183 | * all given class' super- and sub-classes which are all storable. |
| 184 | * Calling this method on non-storable entry will result in an undefined |
| 185 | * result. |
| 186 | */ |
| 187 | public List getRelatedClassEntries(ClassEntry entry) |
| 188 | { |
| 189 | getClassInfo(entry); // Register |
| 190 | synchronized ( this ) |
| 191 | { |
| 192 | List result = (List) relatedClassEntries.get(entry); |
| 193 | if ( result == null ) |
| 194 | return new ArrayList(); |
| 195 | return new ArrayList(result); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Get all subclasses of given entry, including itself. |
| 201 | */ |
| 202 | public List getSubClasses(ClassEntry entry) |
| 203 | { |
| 204 | List relatedEntries = getRelatedClassEntries(entry); |
| 205 | ArrayList result = new ArrayList(); |
| 206 | result.add(entry.getSourceClass()); |
| 207 | for ( int i=0; i<relatedEntries.size(); i++ ) |
| 208 | { |
| 209 | ClassEntry subEntry = (ClassEntry) relatedEntries.get(i); |
| 210 | if ( (entry.getSourceClass().isAssignableFrom(subEntry.getSourceClass())) && |
| 211 | (! result.contains(subEntry.getSourceClass())) ) |
| 212 | result.add(subEntry.getSourceClass()); |
| 213 | } |
| 214 | if ( logger.isDebugEnabled() ) |
| 215 | logger.debug("returning subclasses: "+result+", for: "+entry); |
| 216 | return result; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Get all storable roots for given entry. A storable root for a |
| 221 | * storable entry is itself. A non-storable entry (such as java.lang.Object) |
| 222 | * will have potentially a lot of storable roots: All classes in the |
| 223 | * class hierarchy which are storable, but have non-storable superclasses. |
| 224 | * So, storable roots are the first storable entry in a class hierarchy |
| 225 | * path (roots of the storable sub-forest). When a query is received |
| 226 | * for a non-stored class, the query will split into queries for all |
| 227 | * storable roots. |
| 228 | */ |
| 229 | public List getStorableRootClassEntries(ClassEntry entry) |
| 230 | { |
| 231 | getClassInfo(entry); // Register |
| 232 | synchronized ( this ) |
| 233 | { |
| 234 | List result = (List) rootClassEntries.get(entry); |
| 235 | if ( result == null ) |
| 236 | return new ArrayList(); |
| 237 | return new ArrayList(result); |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * Get a class entry for a class id. |
| 243 | */ |
| 244 | public ClassEntry getClassEntry(Integer id) |
| 245 | { |
| 246 | synchronized ( this ) |
| 247 | { |
| 248 | return (ClassEntry) entriesById.get(id); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Get the id for a class entry. |
| 254 | */ |
| 255 | private Integer getClassEntryId(ClassEntry entry) |
| 256 | { |
| 257 | getClassInfo(entry); // Create classinfo if not yet present |
| 258 | synchronized ( this ) |
| 259 | { |
| 260 | return (Integer)classIdsByEntry.get(entry); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Get the next class id. |
| 266 | */ |
| 267 | public Integer registerClassEntry(ClassEntry entry) |
| 268 | { |
| 269 | // Make it a server call |
| 270 | if ( nodeManager.getRole() == NodeManager.NodeRole.CLIENT ) |
| 271 | { |
| 272 | return (Integer) nodeManager.callServer(ClassTracker.class.getName(), |
| 273 | "registerClassEntry",new Class[] { ClassEntry.class }, new Object[] { entry }); |
| 274 | } |
| 275 | synchronized ( classInfos ) |
| 276 | { |
| 277 | // Try to get class id from an already registered entry, and if |
| 278 | // id is there return it immediately. |
| 279 | Integer classId = (Integer) classIdsByEntry.get(entry); |
| 280 | if ( classId != null ) |
| 281 | return classId; // Entry was already known to server |
| 282 | // Create new id |
| 283 | if ( classMaxId > 65534 ) |
| 284 | throw new StoreException("can not handle this many classes, out of ids, max is 65535."); |
| 285 | classMaxId++; |
| 286 | classId = classMaxId; |
| 287 | // Now create the class entry in the classes table |
| 288 | Transaction transaction = transactionTracker.getTransaction(); |
| 289 | transaction.begin(); |
| 290 | try |
| 291 | { |
| 292 | // Add new class to the classes table. |
| 293 | logger.debug("entry: "+entry+" was not present in database, so adding."); |
| 294 | Map attrs = new HashMap(); |
| 295 | attrs.put("classname",entry.getSourceClass().getName()); |
| 296 | attrs.put("dynamic",entry.getDynamicName()); |
| 297 | attrs.put("id",new Integer(classId)); |
| 298 | attrs.put("maxId",new Long(0)); |
| 299 | database.insert(transaction,"persistence_classes",attrs); |
| 300 | } catch ( StoreException e ) { |
| 301 | transaction.markRollbackOnly(); |
| 302 | throw e; |
| 303 | } catch ( Throwable e ) { |
| 304 | transaction.markRollbackOnly(); |
| 305 | throw new StoreException("unexpected error while trying to create class entry for: "+entry,e); |
| 306 | } finally { |
| 307 | transaction.commit(); |
| 308 | } |
| 309 | // After class row is inserted, return the new id |
| 310 | return classId; |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * Get the next usable id for the given entry, which |
| 316 | * is not yet allocated. These ids can be used to number |
| 317 | * objects of this entry, be sure to "allocate" the ids before |
| 318 | * you use them. |
| 319 | */ |
| 320 | public Long getNextId(ClassEntry entry) |
| 321 | { |
| 322 | getClassInfo(entry); // Create classinfo if not yet present |
| 323 | // Get the beginning of the next interval which is not yet allocated |
| 324 | Long currentId = null; |
| 325 | synchronized ( this ) |
| 326 | { |
| 327 | Long maxId = (Long) maxIdByEntry.get(entry); |
| 328 | // Get the next id which should be given out |
| 329 | currentId = (Long) currentIds.get(entry); |
| 330 | if ( currentId == null ) |
| 331 | currentId = maxId; |
| 332 | if ( logger.isDebugEnabled() ) |
| 333 | logger.debug("generating id for entry: "+entry+", max value: "+maxId+", current: "+currentId); |
| 334 | // If the id is not yet allocated, then allocate a bunch again. Note: |
| 335 | // this is a remote call here, so we have to get the start of the new |
| 336 | // interval too, which is assigned to current id here. |
| 337 | if ( currentId.longValue() >= maxId.longValue() ) |
| 338 | { |
| 339 | // Set the new allocated interval |
| 340 | currentId = allocateObjectIds(entry,ALLOCATE_COUNT); |
| 341 | maxIdByEntry.put(entry,currentId+ALLOCATE_COUNT); |
| 342 | } |
| 343 | // The id is ok, so this should be the id, but also put the |
| 344 | // next one into the free ids. |
| 345 | if ( currentId.longValue()+1 >= MAX_OBJECT_COUNT ) |
| 346 | throw new StoreException("maximum number of objects exceeded for a single table"); |
| 347 | currentIds.put(entry,new Long(currentId.longValue()+1)); |
| 348 | } |
| 349 | // Generate id |
| 350 | Integer classId = getClassEntryId(entry); |
| 351 | Identifier result = new Identifier(classId, currentId); |
| 352 | if ( logger.isDebugEnabled() ) |
| 353 | logger.debug("generated id '"+result+"' for class '"+classId+"' which is: "+entry); |
| 354 | return result.getId(); |
| 355 | } |
| 356 | |
| 357 | /** |
| 358 | * Allocate a given number of ids for the entry. When this method |
| 359 | * returns without exceptions, the given number of ids are successfully |
| 360 | * allocated. This means that now, you have a number range, which is |
| 361 | * guaranteed to be exclusively yours for this entry. |
| 362 | * @return The beginning of the interval allocated. |
| 363 | */ |
| 364 | public long allocateObjectIds(ClassEntry entry, long count) |
| 365 | { |
| 366 | // Make it a server call |
| 367 | if ( nodeManager.getRole() == NodeManager.NodeRole.CLIENT ) |
| 368 | { |
| 369 | return (Long) nodeManager.callServer(ClassTracker.class.getName(), |
| 370 | "allocateObjectIds",new Class[] { ClassEntry.class, long.class }, |
| 371 | new Object[] { entry, count }); |
| 372 | } |
| 373 | // Local method |
| 374 | if ( logger.isDebugEnabled() ) |
| 375 | logger.debug("allocating "+count+" object ids for: "+entry); |
| 376 | getClassInfo(entry); // Initialize class if not yet initialized |
| 377 | synchronized ( this ) |
| 378 | { |
| 379 | Integer classId = (Integer) classIdsByEntry.get(entry); |
| 380 | Long lastId = (Long) maxIdByEntry.get(entry); |
| 381 | if ( (lastId == null) || (classId == null) ) |
| 382 | throw new StoreException("entry '"+entry+"' had no ids yet, it needs to be created first."); |
| 383 | Transaction transaction = transactionTracker.getTransaction(); |
| 384 | transaction.begin(); |
| 385 | try |
| 386 | { |
| 387 | Map keys = new HashMap(); |
| 388 | keys.put("id",getClassEntryId(entry)); |
| 389 | Map attrs = new HashMap(); |
| 390 | attrs.put("maxId",new Long(lastId.longValue()+count)); |
| 391 | database.save(transaction,"persistence_classes",keys,attrs); |
| 392 | } catch ( StoreException e ) { |
| 393 | transaction.markRollbackOnly(); |
| 394 | throw e; |
| 395 | } catch ( Throwable e ) { |
| 396 | transaction.markRollbackOnly(); |
| 397 | throw new StoreException("unexpected error.",e); |
| 398 | } finally { |
| 399 | transaction.commit(); |
| 400 | } |
| 401 | if ( logger.isDebugEnabled() ) |
| 402 | logger.debug("allocated "+count+" ids from "+lastId+" for "+entry); |
| 403 | maxIdByEntry.put(entry,new Long(lastId.longValue()+count)); |
| 404 | return lastId; |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Get class information object of given class. Also, if class |
| 410 | * information does not exist, it will be created. |
| 411 | */ |
| 412 | public ClassInfo getClassInfo(ClassEntry entry) |
| 413 | { |
| 414 | if ( (entry == null) || (entry.getSourceClass()==null) ) |
| 415 | return null; |
| 416 | // Compute full name |
| 417 | String fullClassName = entry.getFullName(); |
| 418 | // Do class searching and creating in a synchronized block |
| 419 | ClassInfo result = null; |
| 420 | synchronized ( classInfos ) |
| 421 | { |
| 422 | // Check whether we can return it quickly from cache |
| 423 | result = (ClassInfo) classInfos.get(fullClassName); |
| 424 | if ( result != null ) |
| 425 | { |
| 426 | if ( logger.isDebugEnabled() ) |
| 427 | logger.debug("class info for entry '"+entry+"' present, returning."); |
| 428 | return result; |
| 429 | } |
| 430 | logger.debug("class info for entry '"+entry+"' is not present"); |
| 431 | // If not, then first make sure the superclass exists recursively |
| 432 | Class clazz = entry.getSourceClass(); |
| 433 | if ( entry.getDynamicName() == null ) |
| 434 | { |
| 435 | // A non-dynamic class has it's superclass as |
| 436 | // superclass (no surprises here) |
| 437 | if ( getType(clazz.getSuperclass()) == ClassType.TYPE_PRIMITIVE ) |
| 438 | throw new StoreException("class "+clazz+" subclassed a primitive type ("+clazz.getSuperclass()+"), this is not allowed."); |
| 439 | if ( clazz.getSuperclass() != null ) |
| 440 | { |
| 441 | getClassInfo(clazz.getSuperclass(),null); |
| 442 | // Now register all interface classes also, so |
| 443 | // we can later select this class with it's interfaces |
| 444 | // also |
| 445 | Class interfaces[] = clazz.getInterfaces(); |
| 446 | for ( int i=0;i<interfaces.length; i++ ) |
| 447 | getClassInfo(interfaces[i],null); |
| 448 | } |
| 449 | } else { |
| 450 | // Dynamic class has the class with 'null' dynamic name |
| 451 | // as superclass. It has no interface classes directly |
| 452 | // associated. |
| 453 | getClassInfo(clazz,null); |
| 454 | } |
| 455 | // Now really create the class info, at this point it is guaranteed |
| 456 | // that the superclasses and superinterfaces are already there. |
| 457 | result = new ClassInfoImpl(this, entry); |
| 458 | // Check whether the class is valid. Note, that check valid |
| 459 | // assumes the class is already registered. So temporaryly |
| 460 | // register it. |
| 461 | try |
| 462 | { |
| 463 | classInfos.put(fullClassName,result); |
| 464 | checkValid(result); |
| 465 | } finally { |
| 466 | classInfos.remove(fullClassName); |
| 467 | } |
| 468 | // Get the class' id from server. Note, if class is not known, |
| 469 | // server will create it. |
| 470 | int classId = registerClassEntry(entry); |
| 471 | // If table operation was successful, update our own data model too |
| 472 | classInfos.put(fullClassName,result); |
| 473 | updateClassEntryRelations(result.getSourceEntry()); |
| 474 | entriesById.put(new Integer(classId),entry); |
| 475 | classIdsByEntry.put(entry,new Integer(classId)); |
| 476 | if ( ! maxIdByEntry.containsKey(entry) ) |
| 477 | maxIdByEntry.put(entry,new Long(0)); |
| 478 | } // End of synchronized block |
| 479 | // Return class info |
| 480 | logger.debug("returning classinfo for entry '"+entry+"': "+result); |
| 481 | return result; |
| 482 | } |
| 483 | |
| 484 | /** |
| 485 | * Determine whether a class is valid. The current algorithm checks: |
| 486 | * <ul> |
| 487 | * <li>Not an inner of anonymous class</li> |
| 488 | * <li>Class can't be empty (abstract and no attributes) and have dynamic attributes.</li> |
| 489 | * <li>Class can't have an attribute which is contained in a related |
| 490 | * class.</li> |
| 491 | * </ul> |
| 492 | * If the class is invalid, a StoreException is thrown with the description |
| 493 | * of the error. |
| 494 | */ |
| 495 | private void checkValid(ClassInfo info) |
| 496 | { |
| 497 | // Non-storables are always valid |
| 498 | if ( ! info.isStorable() ) |
| 499 | return; |
| 500 | // Check that this is not an inner or anonymous class, which are |
| 501 | // not handled, so an error must be generated. |
| 502 | Class clazz = info.getSourceEntry().getSourceClass(); |
| 503 | if ( clazz.getDeclaringClass() != null ) |
| 504 | throw new StoreException("class '"+clazz+"' is an inner or anonymous class, it won't be handled."); |
| 505 | // Check that class has a package |
| 506 | if ( clazz.getPackage() == null ) |
| 507 | throw new StoreException("class '"+clazz+"' had no package given, please define at least one level of package for each class"); |
| 508 | // Check that the object can be constructed, meaning it has a default |
| 509 | // constructor (if it's non-abstract) |
| 510 | if ( (!Modifier.isAbstract(clazz.getModifiers())) && |
| 511 | (getType(clazz)==ClassType.TYPE_OBJECT) ) |
| 512 | { |
| 513 | try |
| 514 | { |
| 515 | clazz.getConstructor(new Class[0]); |
| 516 | } catch ( NoSuchMethodException e ) { |
| 517 | throw new StoreException("class invalid, it had no default constructor: "+clazz); |
| 518 | } |
| 519 | } |
| 520 | // Check emptyness against dynamic attributes |
| 521 | if ( (info.isEmpty()) && (info.hasDynamicAttributes()) ) |
| 522 | throw new StoreException("class info: "+info+" is empty (abstract and has no member fields attributes), but has dynamic attributes. "+ |
| 523 | "This is not allowed, because it could be mistaken for a non-storable class. If it should be stored, please declare it non-abstract, "+ |
| 524 | "or if it has important attributes, declare them as normal fields."); |
| 525 | // Search for common attributes |
| 526 | List<String> attributeNames = info.getAttributeNames(info.getSourceEntry()); |
| 527 | // Validate attribute name format |
| 528 | for ( String attributeName : attributeNames ) |
| 529 | if ( attributeName.endsWith("_underscore") ) |
| 530 | throw new StoreException("attribute can't end with '_underscore', it's reserved, attribute was: "+attributeName); |
| 531 | // Search for the root superentry. If there is no storable superentries, |
| 532 | // then this class obviously has no common attributes with other classes, except |
| 533 | // it's subclasses. |
| 534 | ClassEntry entry = info.getSourceEntry(); |
| 535 | ClassEntry superEntry = entry.getSuperEntry(); |
| 536 | ClassInfo superInfo = getClassInfo(superEntry); |
| 537 | while ( (superEntry!=null) && (superInfo.isStorable()) ) |
| 538 | { |
| 539 | entry = superEntry; |
| 540 | superEntry = entry.getSuperEntry(); |
| 541 | if ( superEntry != null ) |
| 542 | superInfo = getClassInfo(superEntry); |
| 543 | } |
| 544 | // Now check all related classes and ensure, that classes with common |
| 545 | // storable ancestors don't have common attributes. Only if we have a |
| 546 | // storable super. |
| 547 | if ( ! entry.equals(info.getSourceEntry()) ) |
| 548 | { |
| 549 | List relatedEntries = getRelatedClassEntries(entry); |
| 550 | logger.debug("making sure, that no common attributes are present for entry: "+info.getSourceEntry()+", with: "+relatedEntries); |
| 551 | for ( int i=0; i<relatedEntries.size(); i++ ) |
| 552 | { |
| 553 | ClassEntry relatedEntry = (ClassEntry) relatedEntries.get(i); |
| 554 | ClassInfo check = null; |
| 555 | synchronized ( classInfos ) |
| 556 | { |
| 557 | check = (ClassInfo) classInfos.get(relatedEntry.getFullName()); |
| 558 | } |
| 559 | if ( check == null ) |
| 560 | continue; // Classinfo is not yet in, so don't bother |
| 561 | if ( ! check.isStorable() ) |
| 562 | continue; |
| 563 | if ( check.equals(info) ) |
| 564 | continue; |
| 565 | for ( int o=0; o<attributeNames.size(); o++ ) |
| 566 | { |
| 567 | String attributeName = (String) attributeNames.get(o); |
| 568 | if ( check.getAttributeNames(relatedEntry).contains(attributeName) ) |
| 569 | throw new StoreException("class info: "+info+" had a common attribute '"+ |
| 570 | attributeName+"' with '"+check+"', and common attributes in related classes are currently not allowed"); |
| 571 | } |
| 572 | } |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | /** |
| 577 | * Reload the max ids from database. This is called when the connection |
| 578 | * to the server is lost, and the state of the ids is unknown. |
| 579 | */ |
| 580 | private synchronized void reload() |
| 581 | { |
| 582 | // Clear all internal structures |
| 583 | entriesById = new HashMap(); |
| 584 | maxIdByEntry = new HashMap(); |
| 585 | classIdsByEntry = new HashMap(); |
| 586 | // Try to load everything from scratch again |
| 587 | Transaction transaction = transactionTracker.getTransaction(); |
| 588 | transaction.begin(); |
| 589 | try |
| 590 | { |
| 591 | // First ensure that table exists |
| 592 | HashMap tableAttrs = new HashMap(); |
| 593 | tableAttrs.put("classname",String.class); |
| 594 | tableAttrs.put("dynamic",String.class); |
| 595 | tableAttrs.put("id",Integer.class); |
| 596 | tableAttrs.put("maxid",Long.class); |
| 597 | ArrayList tableKeys = new ArrayList(); |
| 598 | tableKeys.add("id"); |
| 599 | database.ensureTable(transaction,"persistence_classes", |
| 600 | tableAttrs,tableKeys,true); |
| 601 | // Load table |
| 602 | logger.debug("reloading all max ids"); |
| 603 | QueryStatement stmt = new QueryStatement("persistence_classes",null,null); |
| 604 | SearchResult result = database.search(transaction,stmt,null); |
| 605 | for ( int i=0; i<result.getResult().size(); i++ ) |
| 606 | { |
| 607 | Map attributes = (Map) result.getResult().get(i); |
| 608 | String className = (String) attributes.get("classname"); |
| 609 | String dynamicName = (String) attributes.get("dynamic"); |
| 610 | Integer id = (Integer) attributes.get("id"); |
| 611 | Long maxId = (Long) attributes.get("maxid"); |
| 612 | if ( id.intValue() > classMaxId ) |
| 613 | classMaxId = id.intValue(); |
| 614 | try |
| 615 | { |
| 616 | Class clazz = Class.forName(className); |
| 617 | ClassEntry entry = new ClassEntry(clazz,dynamicName); |
| 618 | entriesById.put(id,entry); |
| 619 | classIdsByEntry.put(entry,id); |
| 620 | maxIdByEntry.put(entry,maxId); |
| 621 | updateClassEntryRelations(entry); |
| 622 | logger.debug("class tables loaded known class name: "+className+" ("+dynamicName+"), max id: "+maxId); |
| 623 | } catch ( StoreException e ) { |
| 624 | throw e; |
| 625 | } catch ( Exception e ) { |
| 626 | logger.warn("could not find class '"+className+"'. "+ |
| 627 | "This may only mean that a class was removed, but there are traces in the database "+ |
| 628 | "for it, in this case, you may disregard this message: "+e.getMessage()+" ("+e.getClass().getName()+")"); |
| 629 | } |
| 630 | } |
| 631 | } catch ( StoreException e ) { |
| 632 | transaction.markRollbackOnly(); |
| 633 | throw e; |
| 634 | } catch ( Throwable e ) { |
| 635 | transaction.markRollbackOnly(); |
| 636 | throw new StoreException("unexpected error.",e); |
| 637 | } finally { |
| 638 | transaction.commit(); |
| 639 | } |
| 640 | } |
| 641 | |
| 642 | /** |
| 643 | * Update class relations for a given class. This means insert this class |
| 644 | * as related to all subclasses, and mark all subclasses related for this |
| 645 | * class. |
| 646 | */ |
| 647 | private void updateClassEntryRelations(ClassEntry entry) |
| 648 | { |
| 649 | // The first superclass differs when handling dynamic classes, |
| 650 | // because then, the superclass is the ordinary class of dynamic class |
| 651 | ClassEntry superEntry = entry.getSuperEntry(); |
| 652 | // Enter into graph walker as initial node |
| 653 | Stack openEntries = new Stack(); |
| 654 | if ( superEntry != null ) |
| 655 | openEntries.push(superEntry); |
| 656 | // This class will be potentially a storable root for all it's |
| 657 | // interfaces. This is not yet sure, but we will check later. |
| 658 | ArrayList storableSuperEntries = new ArrayList(); |
| 659 | ArrayList nonstorableSuperEntries = new ArrayList(); |
| 660 | for ( int i=0; i<entry.getSourceClass().getInterfaces().length; i++ ) |
| 661 | nonstorableSuperEntries.add( |
| 662 | new ClassEntry(entry.getSourceClass().getInterfaces()[i],null)); |
| 663 | // Go through all superclasses and insert this class |
| 664 | // as related to them. Insert to interfaces and reserved classes |
| 665 | // too, because we need to know if they are related. |
| 666 | // The algorithm is a simple graph walker. All not yet visited |
| 667 | // nodes are stored in openEntries, and visited one-by-one. Note, that |
| 668 | // this works, because there is no cycle in the class hierarchy. |
| 669 | while ( ! openEntries.empty() ) |
| 670 | { |
| 671 | // Get top |
| 672 | superEntry = (ClassEntry) openEntries.pop(); |
| 673 | // Add entry to superclass' related entries |
| 674 | ArrayList classEntries = (ArrayList) relatedClassEntries.get(superEntry); |
| 675 | if ( classEntries == null ) |
| 676 | { |
| 677 | classEntries = new ArrayList(); |
| 678 | relatedClassEntries.put(superEntry,classEntries); |
| 679 | } |
| 680 | if ( ! classEntries.contains(entry) ) |
| 681 | classEntries.add(entry); |
| 682 | // Insert into storable super entries if this entry |
| 683 | // is storable, to non-storable super if it's not. |
| 684 | if ( isStorable(superEntry) ) |
| 685 | { |
| 686 | // Entry is storable, so we will enter this as a related |
| 687 | // class. |
| 688 | storableSuperEntries.add(superEntry); |
| 689 | } else { |
| 690 | // Entry is not storable, so potentially it's first |
| 691 | // storable root is this entry. Of course, if this entry |
| 692 | // has a storable root, then this is not the real root, |
| 693 | // but this will be detected later. |
| 694 | nonstorableSuperEntries.add(superEntry); |
| 695 | } |
| 696 | // Add superclass to open nodes |
| 697 | if ( superEntry.getSourceClass().getSuperclass() != null ) |
| 698 | openEntries.push(new ClassEntry(superEntry.getSourceClass().getSuperclass(),null)); |
| 699 | // Also add all interfaces as not visited |
| 700 | for ( int i=0; i<superEntry.getSourceClass().getInterfaces().length; i++ ) |
| 701 | openEntries.add(new ClassEntry(superEntry.getSourceClass().getInterfaces()[i],null)); |
| 702 | } |
| 703 | if ( logger.isDebugEnabled() ) |
| 704 | logger.debug("class tracker determined superclasses for "+entry+", non-storable: "+nonstorableSuperEntries+", storable: "+storableSuperEntries); |
| 705 | // Now insert all storable superclasses as related to this class. |
| 706 | // If this class is not storable, this should be empty anyway. |
| 707 | ArrayList classEntries = (ArrayList) relatedClassEntries.get(entry); |
| 708 | if ( classEntries == null ) |
| 709 | { |
| 710 | classEntries = new ArrayList(); |
| 711 | relatedClassEntries.put(entry,classEntries); |
| 712 | } |
| 713 | classEntries.removeAll(storableSuperEntries); |
| 714 | classEntries.addAll(storableSuperEntries); |
| 715 | // Now add this class as a storable root to all non-storable |
| 716 | // superclasses and superinterfaces, which do not have a storable |
| 717 | // root which is a superclass to this entry. |
| 718 | // If following condition is false, then this is a non-storable entry, |
| 719 | // so of course non of the non-storable superclasses will have this as storable root. |
| 720 | if ( isStorable(entry) ) |
| 721 | { |
| 722 | // This entry is storable, so it's only storable root is itself |
| 723 | List rootEntries = new ArrayList(); |
| 724 | rootEntries.add(entry); |
| 725 | rootClassEntries.put(entry,rootEntries); |
| 726 | // Now check it's non-storable supers one-by-one to see |
| 727 | // if this class is really their storable root. |
| 728 | for ( int i=0; i<nonstorableSuperEntries.size(); i++ ) |
| 729 | { |
| 730 | ClassEntry nonstorableSuperEntry = (ClassEntry) nonstorableSuperEntries.get(i); |
| 731 | rootEntries = (List) rootClassEntries.get(nonstorableSuperEntry); |
| 732 | if ( rootEntries == null ) |
| 733 | { |
| 734 | // Entry did not have a storable root yet, so this is |
| 735 | // surely a storable root. |
| 736 | rootEntries = new ArrayList(); |
| 737 | rootEntries.add(entry); |
| 738 | rootClassEntries.put(nonstorableSuperEntry,rootEntries); |
| 739 | } else { |
| 740 | // Entry has some storable roots, so check them. |
| 741 | // If a root is a superclass of this class, then this |
| 742 | // class is not the storable root. |
| 743 | // If a root is a subclass of this class, that should not |
| 744 | // happen, because superclasses are always handled first! |
| 745 | boolean hasSuperRoot = false; |
| 746 | for ( int o=0; (o<rootEntries.size()) && (!hasSuperRoot); o++ ) |
| 747 | { |
| 748 | ClassEntry rootEntry = (ClassEntry) rootEntries.get(o); |
| 749 | if ( rootEntry.getSourceClass().equals(entry.getSourceClass()) ) |
| 750 | { |
| 751 | // The class is present as root. If this entry is |
| 752 | // a dynamic subclass of this class, then it can't |
| 753 | // be the root, because it superclass is already root. |
| 754 | hasSuperRoot = true; |
| 755 | } else { |
| 756 | if ( rootEntry.getSourceClass().isAssignableFrom( |
| 757 | entry.getSourceClass()) ) |
| 758 | hasSuperRoot = true; // Real superclass found |
| 759 | if ( entry.getSourceClass().isAssignableFrom( |
| 760 | rootEntry.getSourceClass()) ) |
| 761 | { |
| 762 | // Subclass found, so replace it |
| 763 | logger.debug("replacing storable root "+rootEntry+", with: "+entry); |
| 764 | rootEntries.remove(o--); // Remove this, we found a more suitable root |
| 765 | } |
| 766 | } |
| 767 | } |
| 768 | if ( ! hasSuperRoot ) |
| 769 | { |
| 770 | rootEntries.add(entry); |
| 771 | if ( logger.isDebugEnabled() ) |
| 772 | logger.debug("added "+entry+" as storable root for: "+nonstorableSuperEntry+", roots until now: "+rootEntries); |
| 773 | } |
| 774 | } |
| 775 | } |
| 776 | } |
| 777 | } |
| 778 | |
| 779 | /** |
| 780 | * This determines whether an entry is storable. |
| 781 | * Note: this is also implemented by ClassInfoImpl, but this implementation |
| 782 | * can be used during initialization. |
| 783 | */ |
| 784 | private boolean isStorable(ClassEntry entry) |
| 785 | { |
| 786 | if ( getType(entry.getSourceClass()) == ClassType.TYPE_PRIMITIVE ) |
| 787 | return true; |
| 788 | if ( (entry.getSourceClass().isInterface()) |
| 789 | || (entry.getSourceClass().getName().startsWith("java")) ) |
| 790 | return false; // Intefaces or java.** classes are non-storable |
| 791 | if ( (Modifier.isAbstract(entry.getSourceClass().getModifiers())) |
| 792 | && (!StrictStaticHandler.hasStaticAttributes(entry)) ) |
| 793 | return false; // Abstract superclasses which have no attributes |
| 794 | return true; |
| 795 | } |
| 796 | |
| 797 | /** |
| 798 | * Get a Class instance for a class name postfix. The given parameter |
| 799 | * is treated as a postfix for a fully qualified class name. The postfix |
| 800 | * is considered matching, when it contains whole class of package |
| 801 | * qualifiers. For example: "book" matches "hu.netmind.beankeeper.Book" |
| 802 | * class, but does not match "hu.netmind.beankeeper.CookBook". Also |
| 803 | * "persistence.book" matches "hu.netmind.beankeeper.Book", but |
| 804 | * "tence.book" does not match to previous class.<br> |
| 805 | * If no classes are found null is returned. If more than one matching |
| 806 | * class is present, then one of them is returned (no guarantees which |
| 807 | * one is picked). |
| 808 | * @param postfix The class name postfix. |
| 809 | * @return The class info for which the postfix applies, or null. |
| 810 | */ |
| 811 | public ClassEntry getMatchingClassEntry(String postfix) |
| 812 | { |
| 813 | // Get a list of all known classes from database |
| 814 | HashSet entries; |
| 815 | synchronized ( this ) |
| 816 | { |
| 817 | entries = new HashSet(entriesById.values()); |
| 818 | } |
| 819 | // Search for given prefix |
| 820 | postfix = postfix.toLowerCase(); |
| 821 | Iterator iterator = entries.iterator(); |
| 822 | while ( iterator.hasNext() ) |
| 823 | { |
| 824 | // Check class |
| 825 | ClassEntry entry = (ClassEntry) iterator.next(); |
| 826 | String className = entry.getFullName().toLowerCase(); |
| 827 | if ( (className.endsWith(postfix)) && |
| 828 | ( (className.length()==postfix.length()) || |
| 829 | (className.charAt(className.length()-postfix.length()-1)=='.') ) |
| 830 | ) |
| 831 | return entry; |
| 832 | } |
| 833 | // None found |
| 834 | return null; |
| 835 | } |
| 836 | |
| 837 | /** |
| 838 | * Activate or discard table names added in the transaction. |
| 839 | */ |
| 840 | public void handle(PersistenceEvent event) |
| 841 | { |
| 842 | if ( (event instanceof NodeStateChangeEvent) && |
| 843 | (((NodeStateChangeEvent)event).getNewState()==NodeManager.NodeState.CONNECTED) ) |
| 844 | { |
| 845 | // On each connect reload the database tables |
| 846 | reload(); |
| 847 | } |
| 848 | } |
| 849 | |
| 850 | } |
| 851 | |
| 852 | |