package com.smartgwt.client.util; import java.util.HashMap; import com.smartgwt.client.core.BaseClass; import com.smartgwt.client.core.RefDataClass; import com.smartgwt.client.widgets.BaseWidget; import com.smartgwt.client.widgets.tab.Tab; import com.smartgwt.client.widgets.layout.SectionStackSection; import com.google.gwt.core.client.JavaScriptObject; // ID Management ------------------------------------------------------------------------------- // // The goal of ID Management is to ensure that at most a single SGWT object is assigned the // same ID, and that if a SC object also uses that ID, it can only be the underlying/wrapped // SC object for that SGWT object. The overall system is designed so that: // // - When a SGWT object is created with new, so that there is no expected SC object with the // same ID, then we verify that and warn of a collision if either a SGWT or SC object already // has that ID. Any collision will cause us to destroy() the old SGWT or SC object before // registering the new one. The ID is auto-generated unless specified in the constructor, // and registered against the SGWT object - registration lasts until destroy() is called on // that object. // // - A SC API is used to provide the SGWT auto-generated IDs, so SC will not try to use them, // but the SC side will have no knowledge of any manually set SGWT IDs before the underlying // SC object is created. In this case, a collision will be detected only when the SC // object underlying the SGWT widget is created (say because it's drawn). // // - SC objects created by the SC JS Framework that have no SGWT presence will not be registered // in the SGWT ID Management table, but will still trigger a collision if an attempt is // made to register a SGWT widget with the same ID. // // - When a SGWT object is created from a live SC object, there is obviously no need to check // the SC side for ID collisions, but we still check for collisions against other SGWT // objects. In this case the ID is provided by the SC object and is not generated or // settable. - // // - If setID() is called on a SGWT object, we perform the same collision check as if new // had been called for that ID, and change the registered ID for that object. If setID() // is called for a SGWT object that already has an underlying SC JS object, then // an exception is thrown as this is illegal. // // When destroy() is called on either the SC or SGWT side, both objects will be destroy()ed // if both exist - SC objects can be created outside of SGWT, and SGWT objects may not have // created their underlying SC objects at the point when destroy is called. Destroying a // SGWT object unregisters its ID and destroying a SC object removes the window[ID] binding. // If the ID is auto-generated, calling destroy() (whether both objects exist or just one on // the SC or SGWT side) will return it to the common free pool of auto-generated IDs. public class IDManager { private static int nSmartGwtCollisions; private static int nSmartClientCollisions; // Test for unique IDs: // We need to ensure that IDs // - are valid JS identifiers (no dots, no spaces) // - do not collide with any assigned IDs on the page (EG ID of a live SmartClient // component that was instantiated outside of SmartGWT) // - do not collide with any assigned IDs within SmartGWT components, even if these have // not yet been instantiated as live JavaScript objects. // // We therefore check for window[ID] being already assigned and we track SGWT IDs that // haven't yet been assigned to live JS objects in the global scope by maintaining a // hashSet of assigned IDs private static HashMap<String, Object> assignedIDs = new HashMap<String, Object>(); public static void validateID(String id, boolean skipUniqueJSIdentifierCheck) { assert id.matches("[a-zA-Z_$][0-9a-zA-Z_$]*") : "Invalid ID : " + id + ". Valid ID's must meet the following pattern [a-zA-Z_$][0-9a-zA-Z_$]*"; Object value = assignedIDs.get(id); if (value != null) { SC.logWarn("Specified ID: " + id + " collides with the ID for an existing " + "SmartGWT component or object. The existing object will be " + "destroyed and the ID bound to the new object."); nSmartGwtCollisions++; // The type dispatch could be eliminated by having all three of these // classes implement a common interface, but probably not worth it yet. if (value instanceof BaseWidget) ((BaseWidget) value).destroy(); else if (value instanceof BaseClass) ((BaseClass) value).destroy(); assignedIDs.remove(id); } if (skipUniqueJSIdentifierCheck) return; String collision = checkUniqueJavascriptIdentifier(id); if (collision != null) { SC.logWarn("Specified ID: " + id + " collides with the ID for an existing " + collision + " The existing object will be destroyed and the ID " + "bound to the new object."); nSmartClientCollisions++; destroy(id); } } // If it's an existing JS object it's likely another SmartClient component created outside // of Java, but it could be a native JS keyword ("window", "parent" etc) or something else // defined in JS scope. private static native String checkUniqueJavascriptIdentifier(String id) /*-{ return $wnd[id] != null ? "object '" + $wnd[id] + "'." : null; }-*/; // Any SGWT component should have been cleaned up before this routine is called, so it's // designed to deal with SmartClient components created outside of Java. In the case // of keywords, SmartClient will generate a proper warning; just avoid doing damage here. private static native void destroy(String id) /*-{ var obj = $wnd[id]; if (obj != null && obj.destroy) { delete obj.__sgwtDestroy; obj.destroy(); } else if (!$wnd.isc.ClassFactory._reservedWords[id]) { try { $wnd[id] = null; } catch (e) {} } }-*/; public static void registerID(Object object, String id, boolean skipUniqueJSIdentifierCheck) { if (id == null) { SC.logWarn("Object " + object + " cannot be registered with a null ID"); return; } validateID(id, skipUniqueJSIdentifierCheck); assignedIDs.put(id, object); } // For BaseClass and RefDataClass objects, registration is conditional, // so to simplify the code, it's not an error to blindly request removal, letting // the IDManager logic sort out whether anything has to be done. For BaseWidget // class, registration is mandatory so if it's not found it's a problem. public static void unregisterID(BaseWidget widget, String id) { Object value = assignedIDs.get(id); if (value != widget) { SC.logWarn("The SmartGWT component or object with ID: " + id + " can't be " + "unregistered because another object has registered with the same ID!"); return; } assignedIDs.remove(id); JavaScriptObject config = widget.getConfig(); if (BaseWidget.hasAutoAssignedID(config) && BaseWidget.getRef(config) == null) { SC.releaseID(widget.getClass().getName(), id); } } public static void unregisterID(BaseClass instance, String id) { Object value = assignedIDs.get(id); if (value != instance) return; assignedIDs.remove(id); JavaScriptObject config = instance.getConfig(); if (BaseClass.hasAutoAssignedID(config) && BaseClass.getRef(config) == null) { SC.releaseID(instance.getClass().getName(), id); } } public static void unregisterID(RefDataClass data, String id) { Object value = assignedIDs.get(id); if (value == data) assignedIDs.remove(id); } // Track Symbol Name Collisions public static int getNSmartGwtCollisions() { return nSmartGwtCollisions; } public static int getNSmartClientCollisions() { return nSmartClientCollisions; } }