blob: bc68dcfbe30b4ad414aaa058cad7d63ed631d69f [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.enterprise.adaptor;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* Central creation of objects necessary for the dashboard.
*/
class Dashboard {
private static final Logger log
= Logger.getLogger(Dashboard.class.getName());
private final Config config;
private final Journal journal;
private final CircularLogRpcMethod circularLogRpcMethod
= new CircularLogRpcMethod();
private final GsaCommunicationHandler gsaCommHandler;
private final SessionManager<HttpExchange> sessionManager;
private final RpcHandler rpcHandler;
private final StatRpcMethod statRpcMethod;
public Dashboard(Config config, GsaCommunicationHandler gsaCommHandler,
Journal journal, SessionManager<HttpExchange> sessionManager,
SensitiveValueCodec secureValueCodec, Adaptor adaptor,
List<StatusSource> adaptorSources) {
this.config = config;
this.gsaCommHandler = gsaCommHandler;
this.journal = journal;
this.sessionManager = sessionManager;
List<StatusSource> sources = new LinkedList<StatusSource>();
sources.add(new JavaVersionStatusSource());
sources.add(new LastPushStatusSource(journal));
sources.add(new RetrieverStatusSource(journal));
sources.add(new GsaCrawlingStatusSource(journal));
sources.addAll(adaptorSources);
rpcHandler = new RpcHandler(sessionManager);
rpcHandler.registerRpcMethod("startFeedPush", new StartFeedPushRpcMethod());
rpcHandler.registerRpcMethod("startIncrementalFeedPush",
new StartIncrementalFeedPushRpcMethod());
rpcHandler.registerRpcMethod("getLog", circularLogRpcMethod);
rpcHandler.registerRpcMethod("getConfig", new ConfigRpcMethod(config));
rpcHandler.registerRpcMethod("getStatuses", new StatusRpcMethod(sources));
rpcHandler.registerRpcMethod("checkForUpdatedConfig",
new CheckForUpdatedConfigRpcMethod(gsaCommHandler));
rpcHandler.registerRpcMethod("encodeSensitiveValue",
new EncodeSensitiveValueMethod(secureValueCodec));
statRpcMethod = new StatRpcMethod(journal, adaptor,
gsaCommHandler.isAdaptorIncremental(), config.getConfigFile());
rpcHandler.registerRpcMethod("getStats", statRpcMethod);
}
/** Starts listening for connections to the dashboard. */
public void start(HttpServerScope scope) {
boolean secure = config.isServerSecure();
HttpHandler dashboardHandler = new DashboardHandler();
HttpContext dashboardContext = addFilters(scope.createContext("/dashboard",
createAdminSecurityHandler(dashboardHandler, config, sessionManager,
secure)));
addFilters(scope.createContext("/rpc", createAdminSecurityHandler(
rpcHandler, config, sessionManager, secure)));
addFilters(scope.createContext("/diagnostics-support.zip",
createAdminSecurityHandler(new DownloadDumpHandler(config,
config.getFeedName().replace('_', '-'), statRpcMethod),
config, sessionManager, secure)));
addFilters(scope.createContext("/",
new RedirectHandler(dashboardContext.getPath())));
circularLogRpcMethod.start();
}
private AdministratorSecurityHandler createAdminSecurityHandler(
HttpHandler handler, Config config,
SessionManager<HttpExchange> sessionManager, boolean secure) {
return new AdministratorSecurityHandler(handler, sessionManager,
config.getGsaAdminHostname(), secure);
}
public void stop() {
circularLogRpcMethod.close();
}
private HttpContext addFilters(HttpContext context) {
return gsaCommHandler.addFilters(context);
}
private class StartFeedPushRpcMethod implements RpcHandler.RpcMethod {
@Override
public Object run(List request) {
return gsaCommHandler.checkAndScheduleImmediatePushOfDocIds();
}
}
private class StartIncrementalFeedPushRpcMethod
implements RpcHandler.RpcMethod {
@Override
public Object run(List request) {
return gsaCommHandler.checkAndScheduleIncrementalPushOfDocIds();
}
}
static class CircularLogRpcMethod implements RpcHandler.RpcMethod,
Closeable {
private final CircularBufferHandler circularLog
= new CircularBufferHandler();
/**
* Installs a log handler; to uninstall handler, call {@link #close}.
*/
public void start() {
LogManager.getLogManager().getLogger("").addHandler(circularLog);
}
@Override
public Object run(List request) {
return circularLog.writeOut();
}
@Override
public void close() {
LogManager.getLogManager().getLogger("").removeHandler(circularLog);
}
}
static class ConfigRpcMethod implements RpcHandler.RpcMethod {
private final Config config;
public ConfigRpcMethod(Config config) {
this.config = config;
}
@Override
public Object run(List request) {
TreeMap<String, String> configMap = new TreeMap<String, String>();
for (String key : config.getAllKeys()) {
configMap.put(key, config.getValue(key));
}
return configMap;
}
}
static class StatusRpcMethod implements RpcHandler.RpcMethod {
private final List<StatusSource> sources;
public StatusRpcMethod(List<StatusSource> sources) {
this.sources = Collections.unmodifiableList(
new ArrayList<StatusSource>(sources));
}
public Map<StatusSource, Status> retrieveStatuses() {
Map<StatusSource, Status> statuses
= new LinkedHashMap<StatusSource, Status>(sources.size() * 2);
for (StatusSource source : sources) {
statuses.put(source, source.retrieveStatus());
}
return statuses;
}
@Override
public Object run(List request) {
Map<StatusSource, Status> statuses = retrieveStatuses();
List<Object> flatStatuses = new ArrayList<Object>(statuses.size());
// TODO(ejona): choose locale based on Accept-Languages.
Locale locale = Locale.ENGLISH;
for (Map.Entry<StatusSource, Status> me : statuses.entrySet()) {
Map<String, String> obj = new TreeMap<String, String>();
obj.put("source", me.getKey().getName(locale));
obj.put("code", me.getValue().getCode().name());
obj.put("message", me.getValue().getMessage(locale));
flatStatuses.add(obj);
}
return flatStatuses;
}
}
static class CheckForUpdatedConfigRpcMethod implements RpcHandler.RpcMethod {
private final GsaCommunicationHandler gsaComm;
public CheckForUpdatedConfigRpcMethod(GsaCommunicationHandler gsaComm) {
this.gsaComm = gsaComm;
}
@Override
public Object run(List request) {
return gsaComm.ensureLatestConfigLoaded();
}
}
static class EncodeSensitiveValueMethod implements RpcHandler.RpcMethod {
private final SensitiveValueCodec secureValueCodec;
public EncodeSensitiveValueMethod(SensitiveValueCodec secureValueCodec) {
this.secureValueCodec = secureValueCodec;
}
/**
* Requires two parameters: string to encode and security level.
*/
@Override
public Object run(List request) {
if (request.size() < 2) {
throw new IllegalArgumentException("Required parameters are: string to "
+ "encode and security level");
}
String readable = (String) request.get(0);
if (readable == null) {
throw new NullPointerException("String to encode must not be null");
}
String securityString = (String) request.get(1);
if (securityString == null) {
throw new NullPointerException("Security level must not be null");
}
SensitiveValueCodec.SecurityLevel security;
try {
security = SensitiveValueCodec.SecurityLevel.valueOf(securityString);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Unknown security level", ex);
}
return secureValueCodec.encodeValue(readable, security);
}
}
static class JavaVersionStatusSource implements StatusSource {
@Override
public Status retrieveStatus() {
return retrieveStatus(System.getProperty("java.version"),
System.getProperty("os.name").startsWith("Windows"));
}
Status retrieveStatus(String version, boolean isWindows) {
final String allowedDelimiters = "[\\._\\-]"; // dot, _, or hyphen OK
final String UnixMinimumJVM = "1.6.0_27";
final String WindowsMinimumJVM = "1.7.0_6";
final String minVersion = isWindows ? WindowsMinimumJVM : UnixMinimumJVM;
Scanner versionScanner = new Scanner(version);
Scanner minScanner = new Scanner(minVersion);
versionScanner.useDelimiter(allowedDelimiters);
minScanner.useDelimiter(allowedDelimiters);
while (versionScanner.hasNextInt() && minScanner.hasNextInt()) {
int mine = versionScanner.nextInt();
int needed = minScanner.nextInt();
if (mine < needed) {
return new TranslationStatus(Status.Code.ERROR,
Translation.STATUS_JAVA_VERSION_UNSUPPORTED, version,
minVersion);
}
if (mine > needed) {
return new TranslationStatus(Status.Code.NORMAL,
Translation.STATUS_JAVA_VERSION_SUPPORTED, version);
}
}
/** does supplied version have non-digit component (and thus
{@code hasNextInt()} returns {@code false})? */
if (minScanner.hasNextInt()) {
return new TranslationStatus(Status.Code.WARNING,
Translation.STATUS_JAVA_VERSION_UNKNOWN, version, minVersion);
}
// if we made it here, either the supplied version matched the minimum
// allowed (exactly), or contains additional digit parts -- either is OK.
return new TranslationStatus(Status.Code.NORMAL,
Translation.STATUS_JAVA_VERSION_SUPPORTED, version);
}
@Override
public String getName(Locale locale) {
return Translation.STATUS_JAVA_VERSION.toString(locale);
}
}
static class LastPushStatusSource implements StatusSource {
private final Journal journal;
public LastPushStatusSource(Journal journal) {
this.journal = journal;
}
@Override
public Status retrieveStatus() {
switch (journal.getLastFullPushStatus()) {
case SUCCESS:
return new TranslationStatus(Status.Code.NORMAL);
case INTERRUPTION:
return new TranslationStatus(Status.Code.WARNING,
Translation.STATUS_FEED_INTERRUPTED);
case FAILURE:
default:
return new TranslationStatus(Status.Code.ERROR);
}
}
@Override
public String getName(Locale locale) {
return Translation.STATUS_FEED.toString(locale);
}
}
static class RetrieverStatusSource implements StatusSource {
static final double ERROR_THRESHOLD = 1. / 8.;
static final double WARNING_THRESHOLD = 1. / 16.;
private static final int MAX_COUNT = 1000;
private final Journal journal;
public RetrieverStatusSource(Journal journal) {
this.journal = journal;
}
@Override
public Status retrieveStatus() {
double rate = journal.getRetrieverErrorRate(MAX_COUNT);
Status.Code code;
if (rate >= ERROR_THRESHOLD) {
code = Status.Code.ERROR;
} else if (rate >= WARNING_THRESHOLD) {
code = Status.Code.WARNING;
} else {
code = Status.Code.NORMAL;
}
return new TranslationStatus(code, Translation.STATUS_ERROR_RATE_RATE,
(int) Math.ceil(rate * 100));
}
@Override
public String getName(Locale locale) {
return Translation.STATUS_ERROR_RATE.toString(locale);
}
}
static class GsaCrawlingStatusSource implements StatusSource {
private final Journal journal;
public GsaCrawlingStatusSource(Journal journal) {
this.journal = journal;
}
@Override
public Status retrieveStatus() {
if (journal.hasGsaCrawledWithinLastDay()) {
return new TranslationStatus(Status.Code.NORMAL);
} else {
return new TranslationStatus(Status.Code.WARNING,
Translation.STATUS_CRAWLING_NO_ACCESSES_IN_PAST_DAY);
}
}
@Override
public String getName(Locale locale) {
return Translation.STATUS_CRAWLING.toString(locale);
}
}
}