/*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the license at
* https://glassfish.dev.java.net/public/CDDLv1.0.html or
* glassfish/bootstrap/legal/CDDLv1.0.txt.
* See the License for the specific language governing
* permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at glassfish/bootstrap/legal/CDDLv1.0.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* you own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Copyright 2008 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.enterprise.v3.admin.adapter;
import com.sun.enterprise.config.serverbeans.AdminService;
import com.sun.enterprise.config.serverbeans.Application;
import com.sun.enterprise.config.serverbeans.Domain;
import com.sun.enterprise.config.serverbeans.Property;
import com.sun.enterprise.config.serverbeans.ServerTags;
import com.sun.enterprise.universal.glassfish.SystemPropertyConstants;
import com.sun.grizzly.tcp.http11.GrizzlyAdapter;
import com.sun.grizzly.tcp.http11.GrizzlyOutputBuffer;
import com.sun.grizzly.tcp.http11.GrizzlyRequest;
import com.sun.grizzly.tcp.http11.GrizzlyResponse;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.api.container.Adapter;
import org.glassfish.api.event.EventListener;
import org.glassfish.api.event.Events;
import org.glassfish.api.event.RestrictTo;
import org.glassfish.api.event.EventTypes;
import org.glassfish.internal.api.AdminAuthenticator;
import org.glassfish.server.ServerEnvironmentImpl;
import org.jvnet.hk2.annotations.Inject;
import org.jvnet.hk2.annotations.Service;
import org.jvnet.hk2.component.PostConstruct;
import org.jvnet.hk2.component.Habitat;
/**
* An HK-2 Service that provides the functionality so that admin console access is handled properly.
* The general contract of this adapter is as follows:
*
* - This adapter is *always* installed as a Grizzly adapter for a particular
* URL designated as admin URL in domain.xml. This translates to context-root
* of admin console application.
* - When the control comes to the adapter for the first time, user is asked
* to confirm if downloading the application is OK. In that case, the admin console
* application is downloaded and expanded. While the download and installation
* is happening, all the clients or browser refreshes get a status message.
* No push from the server side is attempted (yet).
* After the application is "installed", ApplicationLoaderService is contacted,
* so that the application is loaded by the containers. This application is
* available as a
system-application and is persisted as
* such in the domain.xml.
* - Even after this application is available, we don't load it on server
* startup by default. It is always loaded
on demand .
* Hence, this adapter will always be available to find
* out if application is loaded and load it in the container(s) if it is not.
* If the application is already loaded, it simply exits.
*
*
*
* @author केदार (km@dev.java.net)
* @author Ken Paulsen (kenpaulsen@dev.java.net)
*
* @since GlassFish V3 (March 2008)
*/
@Service
public final class WebApplicationAdapter extends GrizzlyAdapter implements Adapter, PostConstruct, EventListener {
@Inject
ServerEnvironmentImpl env;
@Inject
AdminService as; //need to take care of injecting the right AdminService
private String contextRoot;
private final List urls = new ArrayList();
private File diskLocation;
private String proxyHost;
private int proxyPort;
private long visitorId;
private AdapterState stateMsg = AdapterState.UNINITIAZED;
private boolean installing = false;
private boolean isOK = false; // FIXME: initialize this with previous user choice
private final CountDownLatch latch = new CountDownLatch(1);
@Inject
private Logger log;
@Inject
Domain domain;
@Inject
Habitat habitat;
@Inject(optional=true)
AdminAuthenticator authenticator=null;
@Inject
Events events;
private String statusHtml;
private String initHtml;
//don't change the following without changing the html pages
private static final String PROXY_HOST_PARAM = "proxyHost";
private static final String PROXY_PORT_PARAM = "proxyPort";
private static final String OK_PARAM = "ok";
//private static final String CANCEL_PARAM = "cancel";
private static final String VISITOR_PARAM = "visitor";
private static final String VISITOR_TOKEN = "%%%VISITOR%%%";
private static final String MYURL_TOKEN = "%%%MYURL%%%";
private static final String STATUS_TOKEN = "%%%STATUS%%%";
static final String ADMIN_APP_NAME = ServerEnvironmentImpl.DEFAULT_ADMIN_CONSOLE_APP_NAME;
//ADMIN_APP_WAR is not related to ADMIN_APP_NAME at all. This is the war file name that is installed through the IPS package.
//This needs to match the tofile attribute of the command in v3/packager-new/glassfish-gui/build.xml
static final String ADMIN_APP_WAR = "admingui.war";
/**
* Constructor.
*/
public WebApplicationAdapter() throws IOException {
initHtml = Utils.packageResource2String("downloadgui.html");
statusHtml = Utils.packageResource2String("status.html");
}
/**
*
*/
public String getContextRoot() {
return contextRoot; //default is /admin
}
/**
*
*/
public void afterService(GrizzlyRequest req, GrizzlyResponse res) throws Exception {
}
/**
*
*/
public void fireAdapterEvent(String type, Object data) {
}
/**
*
*/
public void service(GrizzlyRequest req, GrizzlyResponse res) {
try {
if (!latch.await(100L, TimeUnit.SECONDS)) {
// todo : better error reporting.
log.severe("Cannot process admin console request in time");
return;
}
} catch (InterruptedException ex) {
log.severe("Cannot process admin console request");
return;
}
logRequest(req);
handleAuth(req, res);
if (isApplicationLoaded()) {
handleLoadedState();
} else {
synchronized(this) {
if (isInstalling()) {
sendStatusPage(res);
} else {
if (isApplicationLoaded()) {
// Double check here that it is not now loaded (not
// likely, but possible)
handleLoadedState();
} else if (!hasPermission(req)) {
// Ask for permission
sendConsentPage(req, res);
} else {
try {
// We have permission and now we should install
// (or load) the application.
setInstalling(true);
setStateMsg(AdapterState.INSTALLING);
startThread(); // Thread must set installing false
} catch (Exception ex) {
// Ensure we haven't crashed with the installing
// flag set to true (not likely).
setInstalling(false);
}
sendStatusPage(res);
}
}
}
}
}
/**
*
*/
private boolean isApplicationLoaded() {
return (stateMsg == AdapterState.APPLICATION_LOADED);
}
/**
*
*/
boolean isInstalling() {
return installing;
}
/**
*
*/
void setInstalling(boolean flag) {
installing = flag;
}
/**
* This method sets the current state.
*/
void setStateMsg(AdapterState msg) {
stateMsg = msg;
}
/**
* This method returns the current state, which will be one of the
* following values:
*
* - AdapterSate.UNINITIAZED
* - AdapterSate.INSTALLING
* - AdapterSate.APPLICATION_NOT_INSTALLED
* - AdapterSate.APPLICATION_INSTALLED_BUT_NOT_LOADED
* - AdapterSate.APPLICATION_LOADED
*/
AdapterState getStateMsg() {
return stateMsg;
}
/**
*
*/
public void postConstruct() {
events.register(this);
//set up the environment properly
init();
}
/**
*
*/
public void event(@RestrictTo(EventTypes.SERVER_READY_NAME) Event event) {
latch.countDown();
if (log != null) {
if (log.isLoggable(Level.FINE)) {
log.fine("WebApplicationAdapter is ready.");
}
}
}
/**
*
*/
private void handleAuth(GrizzlyRequest greq, GrizzlyResponse gres) {
try {
File realmFile = new File(env.getProps().get(SystemPropertyConstants.INSTANCE_ROOT_PROPERTY) + "/config/admin-keyfile");
if (authenticator!=null && realmFile.exists()) {
if (!authenticator.authenticate(greq.getRequest(), realmFile)) {
gres.setStatus(HttpURLConnection.HTTP_UNAUTHORIZED);
gres.addHeader("WWW-Authenticate", "BASIC");
gres.finishResponse();
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
*
*/
private void init() {
if ((as == null) || (as.getProperty() == null) || as.getProperty().isEmpty()) {
String msg = "Define following properties in element in domain.xml" +
"for admin console to work properly\n" +
ServerTags.ADMIN_CONSOLE_CONTEXT_ROOT + ", " +
ServerTags.ADMIN_CONSOLE_DOWNLOAD_LOCATION + ", " +
ServerTags.ADMIN_CONSOLE_LOCATION_ON_DISK;
log.info(msg);
return;
}
List props = as.getProperty();
for (Property prop : props) {
setContextRoot(prop);
//setDownloadLocations(prop);
setLocationOnDisk(prop);
}
initState();
}
/**
*
*/
private void initState() {
// It is a given that the application is NOT loaded to begin with
if (appExistsInConfig()) {
setStateMsg(AdapterState.APPLICATION_INSTALLED_BUT_NOT_LOADED);
} else {
setStateMsg(AdapterState.APPLICATION_NOT_INSTALLED);
}
}
/**
*
*/
private boolean appExistsInConfig() {
return (getConfig() != null);
}
/**
*
*/
Application getConfig() {
//no application-ref logic here -- that's on purpose for now
Application app = domain.getSystemApplicationReferencedFrom(env.getInstanceName(), ADMIN_APP_NAME);
return app;
}
/**
*
*/
private void logRequest(GrizzlyRequest req) {
// FIXME: Change all INFO to FINE
log.info("WebApplicationAdapter's STATE IS: " + getStateMsg());
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Current Thread: " + Thread.currentThread().getName());
Enumeration names = req.getParameterNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String values = Arrays.toString(req.getParameterValues(name));
log.fine("Parameter name: " + name + " values: " + values);
}
}
}
/**
*
*/
private void setContextRoot(Property prop) {
if (prop == null) {
contextRoot = ServerEnvironmentImpl.DEFAULT_ADMIN_CONSOLE_CONTEXT_ROOT;
return;
}
if (ServerTags.ADMIN_CONSOLE_CONTEXT_ROOT.equals(prop.getName())) {
if ((prop.getValue() != null) && prop.getValue().startsWith("/")) {
contextRoot = prop.getValue();
log.info("Admin Console Adapter: context root: " + contextRoot);
} else {
log.info("Invalid context root for the admin console application, using default:" + ServerEnvironmentImpl.DEFAULT_ADMIN_CONSOLE_CONTEXT_ROOT);
contextRoot = ServerEnvironmentImpl.DEFAULT_ADMIN_CONSOLE_CONTEXT_ROOT;
}
}
}
/*
private void setDownloadLocations(Property prop) {
if (ServerTags.ADMIN_CONSOLE_DOWNLOAD_LOCATION.equals(prop.getName())) {
String value = prop.getValue();
if (value != null && !"".equals(value)) {
Pattern sp = Pattern.compile("\\|");
String[] strings = sp.split(value);
for (String s : strings) {
try {
urls.add(new URL(s));
logFine(s);
} catch (MalformedURLException me) {
log.info("Ignored invalid URL format: " + s);
}
}
} else {
log.info("The value for: " + prop.getName() + " is invalid");
}
}
}
*/
/**
*
*/
private void setLocationOnDisk(Property prop) {
if (ServerTags.ADMIN_CONSOLE_LOCATION_ON_DISK.equals(prop.getName())) {
if (prop.getValue() != null) {
diskLocation = new File(prop.getValue());
//System.out.println("Admin Console will be downloaded to: " + diskLocation.getAbsolutePath());
logFine("Admin Console will be downloaded to: " + diskLocation.getAbsolutePath());
if (!diskLocation.canWrite()) {
log.warning(diskLocation.getAbsolutePath() + " can't be written to, download will fail");
}
}
}
}
/**
*
*/
enum InteractionResult {
OK,
CANCEL,
FIRST_TIMER;
}
/**
* Determines if the user has permission.
*/
private boolean hasPermission(GrizzlyRequest req) {
//do this quickly as this is going to block the grizzly worker thread!
//check for returning user?
InteractionResult ir = getUserInteractionResult(req);
if (ir == InteractionResult.OK) {
// FIXME: I need to "remember" this answer in a persistent way!! Or it will popup this message EVERY time after the server restarts.
isOK = true;
}
return isOK;
}
/**
*
*/
private void startThread() {
new InstallerThread(urls, diskLocation, proxyHost, proxyPort, this, habitat, domain, env, contextRoot, log).start();
}
/**
*
*/
private synchronized InteractionResult getUserInteractionResult(GrizzlyRequest req) {
String v = visitorId + "";
if ((req.getParameter(VISITOR_PARAM) != null) && (v.equals(req.getParameter(VISITOR_PARAM)))) {
if (req.getParameter(OK_PARAM) != null) {
proxyHost = req.getParameter(PROXY_HOST_PARAM);
if (proxyHost != null) {
String ps = req.getParameter(PROXY_PORT_PARAM);
try {
proxyPort = Integer.parseInt(ps);
} catch (NumberFormatException nfe) {
//ignore
}
}
return InteractionResult.OK;
} else { //canceled
return InteractionResult.CANCEL;
}
}
// This is a first-timer
return InteractionResult.FIRST_TIMER;
}
/**
*
*/
private synchronized void sendConsentPage(GrizzlyRequest req, GrizzlyResponse res) { //should have only one caller
GrizzlyOutputBuffer ob = res.getOutputBuffer();
res.setStatus(200);
res.setContentType("text/html");
byte[] bytes;
try {
try {
String hp = (contextRoot.startsWith("/")) ? "" : "/";
hp += contextRoot + "/";
visitorId = System.currentTimeMillis(); //sufficiently unique
bytes = initHtml.replace(MYURL_TOKEN, hp).replace(VISITOR_TOKEN, visitorId+"").getBytes();
} catch (Exception ex) {
bytes = ("Catastrophe:" + ex.getMessage()).getBytes();
}
res.setContentLength(bytes.length);
ob.write(bytes, 0, bytes.length);
ob.flush();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
*
*/
private void sendStatusPage(GrizzlyResponse res) {
GrizzlyOutputBuffer ob = res.getOutputBuffer();
res.setStatus(200);
res.setContentType("text/html");
byte[] bytes;
try {
String status = "" + getStateMsg();
bytes = statusHtml.replace(STATUS_TOKEN, status).getBytes();
res.setContentLength(bytes.length);
ob.write(bytes, 0, bytes.length);
ob.flush();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
*
*/
private void handleLoadedState() {
// do nothing
statusHtml = null;
initHtml = null;
}
/**
*
*/
private void logFine(String s) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, s);
}
}
}