/* * Copyright (C) NetStruxr, Inc. All rights reserved. * * This software is published under the terms of the NetStruxr * Public Software License version 0.5, a copy of which has been * included with this distribution in the LICENSE.NPL file. */ /* ERSharedEOLoader.java created by max on Wed 07-Mar-2001 */ package er.extensions.eof; import java.util.Enumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOAdaptorContext; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOModel; import com.webobjects.eoaccess.EOModelGroup; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOObjectStoreCoordinator; import com.webobjects.eocontrol.EOSharedEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import er.extensions.foundation.ERXRetainer; // Note: This is a direct port of Kelly Hawks' ObjC SharedEOLoader. Only enhanced it to use the log4j system. // // Kelly's description of the problem: // // Now on to the problem. Without source code and symbols, I can't say exactly // where things go wrong, but the bug occurs when: // 1) you have an application with more than one model in a model group // 2) there are cross-model relationships // 3) The first object you fetch from the database is from a model that doesn't // contain shared objects. What seems to be happening is EOF's shared EO loader goes like this: // 1) request comes in for object from model #1 // 2) shared EOs are loaded for model #1 (none). // 3) object from step #1 touches a relationship to an entity in model #2. // 4) shared EOs are loaded for model #2. // For some reason, this is too late. All the shared EOs for all models need to // be loaded at once. // The bug manifests itself as: // Aug 13 00:07:22 FooApplication [24239] *** Uncaught exception: // <NSInternalInconsistencyException> sqlStringForKeyValueQualifier:: attempt // to generate SQL for EOKeyValueQualifier 0x441298 // '(someRelationship.someAttribute = 'someValue')' failed because attribute // identified by key 'someRelationship.someAttribute' was not reachable from // from entity 'someEntity' // CHECKME: I believe this bug has been fixed, should be removed if this is the case. /** * Java port of Kelly Hawk's shared EO loader. Works around a bug with shared eos and multiple models. */ public class ERXSharedEOLoader { private static final Logger log = LoggerFactory.getLogger("er.extensions.fixes.ERSharedEOLoader"); /** holds the key to enable patched shared eo loading */ public static final String PatchSharedEOLoadingPropertyKey = "er.extensions.ERXSharedEOLoader.PatchSharedEOLoading"; public static ERXSharedEOLoader _defaultLoader; protected static boolean _loadingComplete = false; // enables the patch by creating a ERSharedEOLoader object and disabling // EOF's sharedObjectLoading via a call to EODatabaseContext. public static void patchSharedEOLoading() { if (_defaultLoader == null) { EODatabaseContext.setSharedObjectLoadingEnabled(false); _defaultLoader = new ERXSharedEOLoader(); ERXRetainer.retain(_defaultLoader); // Needs to be retained on the objC side to recieve notifications. log.debug("Shared EO loading patch installed."); } } // This disables the patch, but leaves shared object loading off. // To completely restore default EOF behavior, call // [EODatabaseContext setSharedObjectLoadingEnabled:YES]; public static void removeSharedEOLoadingPatch() { if (_defaultLoader != null) { ERXRetainer.release(_defaultLoader); _defaultLoader = null; } if (_loadingComplete) { log.debug("the patch has been removed, but after shared EO loading completed; this call had no effect"); } else { log.debug("shared EO loading patch UNINSTALLED."); } } protected NSMutableArray _modelList = new NSMutableArray(); protected int _transCount = 0; protected boolean _didChangeDebugSetting = false; protected EOAdaptorContext _currentAdaptor; // Sets the receiver up as an observer for the EOModelAddedNotification // and the EOCooperatingObjectStoreWasAdded notification. public ERXSharedEOLoader() { NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("modelWasAddedNotification", ERXConstant.NotificationClassArray), EOModelGroup.ModelAddedNotification, null); NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("objectStoreWasAdded", ERXConstant.NotificationClassArray), EOObjectStoreCoordinator.CooperatingObjectStoreWasAddedNotification, null); } @Override public void finalize() throws Throwable { NSNotificationCenter.defaultCenter().removeObserver(this); super.finalize(); } // actually carries out the loading of shared EOs. public void loadSharedObjectsForModel(EOModel aModel) { NSArray entities = aModel.entitiesWithSharedObjects(); if (entities != null && entities.count() > 0) { // calling defaultSharedEditingContext "turns on" sharing, so don't // call it unless we know there are objects to preload. EOSharedEditingContext dsec = EOSharedEditingContext.defaultSharedEditingContext(); if (dsec != null) { // Load the shared EOs for (Enumeration e = entities.objectEnumerator(); e.hasMoreElements();) { EOEntity entity = (EOEntity)e.nextElement(); /* For EOs that are completely shared the below for loop results in *2* fetches for 1 fs (fetchAll) when the entity is caching the code below works around this with only 1 fetch. */ if (entity.sharedObjectFetchSpecificationNames().count() == 1 && entity.sharedObjectFetchSpecificationNames().lastObject().equals("FetchAll")) { try { EOFetchSpecification fs = entity.fetchSpecificationNamed("FetchAll"); dsec.bindObjectsWithFetchSpecification(fs, "FetchAll"); } catch (Exception e1) { log.error("Exception occurred for entity named: {} in Model: {}", entity.name(), aModel.name(), e1); throw new RuntimeException(e.toString()); } } else { for (Enumeration ee = entity.sharedObjectFetchSpecificationNames().objectEnumerator(); ee.hasMoreElements();) { String fsn = (String)ee.nextElement(); EOFetchSpecification fs = entity.fetchSpecificationNamed(fsn); if (fs != null) { log.debug("Loading {} - {}", entity.name(), fsn); dsec.bindObjectsWithFetchSpecification(fs, fsn); } } } } } } } // As models are added to the application, this method receives // notifications about them and stores them off to an array. public void modelWasAddedNotification(NSNotification aNotification) { // sometimes a model gets added twice; make sure we store it once. if (!_modelList.containsObject(aNotification.object())) { log.debug("Adding model: {}", ((EOModel)aNotification.object()).name()); _modelList.addObject(aNotification.object()); } } // The first time an object store is added to EOF, this class iterates // over all the models that heard about via EOModelAddedNotification's // and loads all their shared EOs at once. public void objectStoreWasAdded(NSNotification aNotification) { if (!_loadingComplete) { if (_modelList.count() == 0) { EOModelGroup group = EOModelGroup.modelGroupForObjectStoreCoordinator((EOObjectStoreCoordinator)aNotification.object()); if (group != null && group.models().count() == 0) { throw new RuntimeException("No models found in default group"); } // internal list empty; drop it and use modelgroup's. _modelList = new NSMutableArray(group.models()); } _loadingComplete = true; // make sure we only do this once. EOModel currentModel = null; try { NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("transactionBeginning", ERXConstant.NotificationClassArray), EOAdaptorContext.AdaptorContextBeginTransactionNotification, null); log.debug("Beginning loading of shared EOs"); NSMutableArray loadedModels = new NSMutableArray(); for (Enumeration e = _modelList.objectEnumerator(); e.hasMoreElements();) { currentModel = (EOModel)e.nextElement(); if (!loadedModels.containsObject(currentModel.name())) { loadSharedObjectsForModel(currentModel); loadedModels.addObject(currentModel.name()); } } NSNotificationCenter.defaultCenter().removeObserver(this, EOAdaptorContext.AdaptorContextBeginTransactionNotification, null); if (_didChangeDebugSetting) { //_currentAdaptor.setDebugEnabled(true); _didChangeDebugSetting = false; } if (_transCount != 0) { // only print this if we loaded something; otherwise // the request for the reg. obj. count with start sharing. log.debug("Shared EO loading complete: {} transactions/ {} objects.", _transCount, EOSharedEditingContext.defaultSharedEditingContext().registeredObjects().count()); } else { log.debug("Shared EO loading complete: no objects loaded."); } } catch (Exception e) { log.error("Exception occurred with model: {}", currentModel, e); // no matter what happens, un-register for notifications. NSNotificationCenter.defaultCenter().removeObserver(this, EOAdaptorContext.AdaptorContextBeginTransactionNotification, null); if (_didChangeDebugSetting) { //_currentAdaptor.setDebugEnabled(true); _didChangeDebugSetting = false; } } } } // The method catches EOAdaptorContextBeginTransactionNotification // notifications while the loading of shared objects is happening and // shuts off debugging messages if the adaptor has debugging enabled. public void transactionBeginning(NSNotification aNotification) { _currentAdaptor = (EOAdaptorContext)aNotification.object(); /* if (_currentAdaptor.isDebugEnabled() && !ERXExtensions.sharedEOAdaptorCategory.isDebugEnabled()) { _didChangeDebugSetting = true; _currentAdaptor.setDebugEnabled(false); log.debug("Disabling adaptor debugging while loading shared EOs..."); } */ _transCount++; } }