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;
}
}