/*******************************************************************************
* Copyright 2011 Google Inc. All Rights Reserved.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.google.gwt.eclipse.oophm.model;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents a browser tab associated with a launch configuration.
*/
public class BrowserTab implements IModelNode, INeedsAttention {
/**
* Holds information about the browser tab.
*/
public static class Info {
private final byte[] browserIconBytes;
private final String remoteHost;
private final String initialSessionKey;
private final String tabKey;
private final String url;
private final String userAgentTag;
/**
* Construct a new instance.
*
* @param tabKey stable browser tab identifier
* @param userAgentTag short-form user agent identifier, suitable for use in a
* label for this connection
* @param url URL of top-level window
* @param remoteHost The browser socket endpoint, in host:port format,
* that is communicating with the Development Mode server.
* @param initialSessionKey the session key for the first session that was
* loaded in this tab
* @param browserIconBytes icon to use for the user agent (fits inside
* 24x24) or null if unavailable
*/
public Info(String tabKey, String userAgentTag, String url,
String remoteHost, String initialSessionKey, byte[] browserIconBytes) {
this.tabKey = returnEmptyStrIfNull(tabKey);
this.userAgentTag = generateUniqueNameIfNullOrEmpty(userAgentTag,
"Browser");
this.remoteHost = returnEmptyStrIfNull(remoteHost);
this.url = generateUniqueNameIfNullOrEmpty(url, "URL");
this.initialSessionKey = returnEmptyStrIfNull(initialSessionKey);
this.browserIconBytes = browserIconBytes;
}
/**
* Return icon to use for the user agent (fits inside 24x24) or null if
* unavailable
*/
public byte[] getBrowserIconBytes() {
return browserIconBytes;
}
/**
* Return the browser socket endpoint, in host:port format, that is being
* used to communicate with the Development Mode server, or "Unknown".
*
* @return the remote host as a string formatted as host:port
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Return a stable browser tab identifier for this tab, or the empty string.
*/
public String getTabKey() {
return tabKey;
}
/**
* Return the top-level URL for the tab.
*/
public String getUrl() {
return url;
}
/**
* Return a short-form user agent identifier suitable for use in a label.
*/
public String getUserAgentTag() {
return userAgentTag;
}
/**
* Return the key identifying the first session that was loaded in this tab.
* Note that after the tab is refreshed, this value will not reflect the
* session for the current session. This method is only useful when tabs do
* not have an identifier, and there is only one session per browser tab.
* Otherwise, it is best to look at the session key for a given module.
*/
String getInitialSessionKey() {
return initialSessionKey;
}
}
/**
* A reference to a module instance in a browser tab. A module instance is
* identified by its name and its session key.
*/
public static class ModuleHandle {
private final String moduleName;
private final String sessionKey;
public ModuleHandle(String moduleName, String sessionKey) {
this.moduleName = moduleName;
this.sessionKey = sessionKey;
}
public String getName() {
return moduleName;
}
public String getSessionKey() {
return sessionKey;
}
}
private static final AtomicInteger nextIdForUnspecifiedValue = new AtomicInteger(
1);
/**
* Computes a tab name prefix for the {@link Info} instance. This name may be
* augmented by the caller to guarantee uniqueness within the
* {@link LaunchConfiguration}.
*/
static String computeNamePrefix(Info info) {
String path;
try {
URL url = new URL(info.getUrl());
path = url.getPath();
if (path.startsWith("/") && path.length() > 1) {
path = path.substring(1);
}
} catch (MalformedURLException e) {
path = info.getUrl();
}
return path + " - " + info.getUserAgentTag();
}
private static String generateUniqueNameIfNullOrEmpty(String str,
String prefix) {
if (str == null || str.length() == 0) {
return prefix + " " + nextIdForUnspecifiedValue.getAndIncrement();
}
return str;
}
private static String returnEmptyStrIfNull(String str) {
if (str == null) {
return "";
}
return str;
}
private final int id;
private boolean isTerminated = false;
private String needsAttentionLevel = null;
private final LaunchConfiguration launchConfiguration;
private final Log<BrowserTab> log = new Log<BrowserTab>(this);
private final List<ModuleHandle> modules = new ArrayList<ModuleHandle>();
private final String name;
private final Object privateInstanceLock = new Object();
private final Info tabInfo;
/**
* Create a new browser tab instance.
*
* @param launchConfiguration The launch configuration associated with this
* browser tab instance
* @param info information about the browser tab and the modules that it's
* loaded
* @param name the name of this tab
* @param moduleName initial module name
*/
BrowserTab(LaunchConfiguration launchConfiguration, Info info, String name,
String moduleName) {
id = launchConfiguration.getModel().getModelNodeNextId();
this.launchConfiguration = launchConfiguration;
this.tabInfo = info;
this.name = name;
addModule(moduleName, info.getInitialSessionKey());
}
/**
* Add a module that was loaded in this browser tab. If the module name is
* null or the empty string, a name will be generated. It is legal to add a
* module with the same name and session key more than once.
*
* If this browser tab was marked as terminated, then it will be reset to the
* unterminated state. An event will be fired to all listeners on the model.
*
* NOTE: This method fires events. If you're invoking this method from other
* model classes, make sure that no locks are being held.
*
* @param moduleName the name of the module
* @param sessionKey the session in which this module was loaded
* @return a handle to the loaded module
*/
public ModuleHandle addModule(String moduleName, String sessionKey) {
String sanitizedModuleName = generateUniqueNameIfNullOrEmpty(moduleName,
"Module");
ModuleHandle moduleHandle = new ModuleHandle(sanitizedModuleName,
sessionKey);
synchronized (privateInstanceLock) {
modules.add(moduleHandle);
}
setTerminated(false);
return moduleHandle;
}
/**
* Mark all log entries for this element as being undisclosed, and clears the
* attention level of this node.
*
* Fires removal events and a needs attention event to all listeners of this
* log.
*
* NOTE: This method fires events. If you're invoking this method from other
* model classes, make sure that no locks are being held.
*/
public void clearLog() {
getLog().undiscloseAllLogEntries();
setNeedsAttentionLevel(null);
}
public List<IModelNode> getChildren() {
return Collections.emptyList();
}
public int getId() {
return id;
}
/**
* Return detailed information about this browser tab.
*/
public Info getInfo() {
return tabInfo;
}
/**
* Returns the launch configuration associated with this browser tab.
*/
public LaunchConfiguration getLaunchConfiguration() {
return launchConfiguration;
}
/**
* Return the log associated with this browser tab.
*/
public Log<BrowserTab> getLog() {
return log;
}
/**
* Return a list of the names of the modules that were loaded in this browser
* tab.
*/
public List<ModuleHandle> getModules() {
synchronized (privateInstanceLock) {
return new ArrayList<ModuleHandle>(modules);
}
}
/**
* Return the name of the browser tab.
*/
public String getName() {
return name;
}
public String getNeedsAttentionLevel() {
synchronized (privateInstanceLock) {
return needsAttentionLevel;
}
}
public LaunchConfiguration getParent() {
return getLaunchConfiguration();
}
/**
* Returns whether or not the browser tab has been terminated.
*/
public boolean isTerminated() {
synchronized (privateInstanceLock) {
return isTerminated;
}
}
/**
* Removes a module that is no longer loaded in this browser tab. If, after
* the removal of the module from the list of loaded modules, this browser tab
* has no loaded modules, it will be marked as terminated, and an event will
* be reported to all listeners on the model.
*
* There should only be a single module loaded in this tab that matches the
* given module handle.
*
* NOTE: This method fires events. If you're invoking this method from other
* model classes, make sure that no locks are being held.
*
* @param moduleHandle a handle to the module that was unloaded. This handle
* must have been one that was returned by a call to
* {@link #addModule(String, String)} on this browser tab instance
* @return true if the module existed in the list of loaded modules and was
* removed, false otherwise
*/
public boolean removeModule(ModuleHandle moduleHandle) {
boolean noLoadedModules = false;
boolean wasModuleRemoved = false;
synchronized (privateInstanceLock) {
if (modules.remove(moduleHandle)) {
wasModuleRemoved = true;
noLoadedModules = (modules.size() == 0);
}
}
if (noLoadedModules) {
setTerminated(true);
}
return wasModuleRemoved;
}
public void setNeedsAttentionLevel(String needsAttentionLevel) {
synchronized (privateInstanceLock) {
// We shouldn't be raising the attn level if terminated, but we should
// still allow setTerminated() to clear it (needsAttentionLevel == null).
if (!AttentionLevelUtils.isNewAttnLevelMoreImportantThanOldAttnLevel(
this.needsAttentionLevel, needsAttentionLevel)
|| (isTerminated() && needsAttentionLevel != null)) {
return;
}
this.needsAttentionLevel = needsAttentionLevel;
}
final WebAppDebugModelEvent<BrowserTab> browserTabNeedsAttentionEvent = new WebAppDebugModelEvent<BrowserTab>(
this);
fireBrowserTabNeedsAttention(browserTabNeedsAttentionEvent);
}
/**
* Flag this browser tab as terminated. Removes all other terminated tabs
* (except the most-recently terminated tab) with a name matching that of this
* tab. Clears the attention level of this node.
*
* Fires a termination event, possibly an attention event, and possibly
* removal events to all listeners on the {@link WebAppDebugModel}.
*
* NOTE: This method fires events. If you're invoking this method from other
* model classes, make sure that no locks are being held.
*/
public void setTerminated() {
setTerminated(true);
}
private void fireBrowserTabNeedsAttention(
WebAppDebugModelEvent<BrowserTab> browserTabNeedsAttentionEvent) {
for (IWebAppDebugModelListener webAppDebugModelListener : getLaunchConfiguration().getModel().getWebAppDebugModelListeners()) {
webAppDebugModelListener.browserTabNeedsAttention(browserTabNeedsAttentionEvent);
}
}
private void fireBrowserTabTerminated(
WebAppDebugModelEvent<BrowserTab> browserTabTerminatedEvent) {
for (IWebAppDebugModelListener webAppDebugModelListener : getLaunchConfiguration().getModel().getWebAppDebugModelListeners()) {
webAppDebugModelListener.browserTabTerminated(browserTabTerminatedEvent);
}
}
/**
* Set the termination state of this browser tab. If <code>terminated</code>
* is true, and this browser tab is not already terminated, then all other
* terminated tabs (except the most-recently terminated tab) with a name
* matching that of this tab with be removed from the model. The attention
* level of this node will also be cleared. A termination event, possibly an
* attention event, and possibly removal events to all listeners on the
* {@link WebAppDebugModel} will be fired.
*
* If <code>terminated</code> is false, and this browser tab is marked as
* terminated, then a termination state change event will be reported to all
* listeners on the model.
*
* NOTE: This method fires events. If you're invoking this method from other
* model classes, make sure that no locks are being held.
*/
private void setTerminated(boolean terminated) {
synchronized (privateInstanceLock) {
if (isTerminated == terminated) {
return;
}
this.isTerminated = terminated;
}
final WebAppDebugModelEvent<BrowserTab> browserTabTerminatedEvent = new WebAppDebugModelEvent<BrowserTab>(
this);
fireBrowserTabTerminated(browserTabTerminatedEvent);
if (terminated) {
setNeedsAttentionLevel(null);
/*
* On a browser refresh, we're informed about the new session far quicker
* than the termination of the existing session. As a result, we miss the
* opportunity to clean up previous terminated sessions when the new
* session is added, because at that point, the previous session has not
* been marked as terminated as yet.
*
* To combat this problem, we'll perform a cleanup of terminated launches
* whenever we receive a termination notification, in addition to whenever
* we're informed that a new session has started.
*/
launchConfiguration.removeAllAssociatedTerminatedTabsExceptMostRecent(this);
}
}
}