/*
###############################################################################
# #
# Copyright (C) 2011-2016 OpenMEAP, Inc. #
# Credits to Jonathan Schang & Rob Thacher #
# #
# Released under the LGPLv3 #
# #
# OpenMEAP is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Lesser General Public License as published #
# by the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# OpenMEAP is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU Lesser General Public License for more details. #
# #
# You should have received a copy of the GNU Lesser General Public License #
# along with OpenMEAP. If not, see <http://www.gnu.org/licenses/>. #
# #
###############################################################################
*/
package com.openmeap.services;
import static com.openmeap.util.ParameterMapUtils.firstValue;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpStatus;
import com.openmeap.thirdparty.org.json.me.JSONException;
import com.openmeap.thirdparty.org.json.me.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import com.openmeap.cluster.dto.ClusterNodeRequest;
import com.openmeap.constants.FormConstants;
import com.openmeap.constants.UrlParamConstants;
import com.openmeap.digest.DigestException;
import com.openmeap.event.Event;
import com.openmeap.event.EventHandler;
import com.openmeap.event.EventHandlingException;
import com.openmeap.json.JSONObjectBuilder;
import com.openmeap.model.InvalidPropertiesException;
import com.openmeap.model.ModelManager;
import com.openmeap.model.ModelServiceImpl;
import com.openmeap.model.dto.ApplicationArchive;
import com.openmeap.model.dto.ClusterNode;
import com.openmeap.model.dto.GlobalSettings;
import com.openmeap.model.event.MapPayloadEvent;
import com.openmeap.model.event.ModelEntityEventAction;
import com.openmeap.model.event.handler.ArchiveFileDeleteHandler;
import com.openmeap.model.event.handler.ArchiveFileUploadHandler;
import com.openmeap.model.event.handler.ModelServiceRefreshHandler;
import com.openmeap.services.dto.Result;
import com.openmeap.util.AuthTokenProvider;
import com.openmeap.util.GenericRuntimeException;
import com.openmeap.util.ParameterMapUtils;
import com.openmeap.util.ServletUtils;
import com.openmeap.util.Utils;
/**
* Used to notify that model items have been modified in the administrative interface.
*
* Because the admin server is separated from the web-services,
* we needed to create a means of notifying the web-services that
* items in the persistence context were stale.
*
* @author schang
*/
public class ServiceManagementServlet extends HttpServlet {
private static final long serialVersionUID = 3528435164985362736L;
private Logger logger = LoggerFactory.getLogger(ServiceManagementServlet.class);
private ModelManager modelManager = null;
private ModelServiceRefreshHandler modelServiceRefreshHandler = null;
private ArchiveFileUploadHandler archiveUploadHandler = null;
private ArchiveFileDeleteHandler archiveDeleteHandler = null;
private WebApplicationContext context = null;
public void init() {
context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
modelManager = (ModelManager)context.getBean("modelManager");
modelServiceRefreshHandler = (ModelServiceRefreshHandler)context.getBean("modelServiceRefreshHandler");
archiveUploadHandler = (ArchiveFileUploadHandler)context.getBean("archiveUploadHandler");
archiveDeleteHandler = (ArchiveFileDeleteHandler)context.getBean("archiveDeleteHandler");
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
Result result = null;
PrintWriter os = new PrintWriter(response.getOutputStream());
logger.debug("Request uri: {}",request.getRequestURI());
logger.debug("Request url: {}",request.getRequestURL());
logger.debug("Query string: {}",request.getQueryString());
if(logger.isDebugEnabled()) {
logger.debug("Parameter map: {}",ParameterMapUtils.toString(request.getParameterMap()));
}
String action = request.getParameter(UrlParamConstants.ACTION);
if( action==null ) {
action="";
}
if( ! authenticates(request) ) {
logger.error("Request failed to authenticate ",request);
result = new Result(Result.Status.FAILURE,"Authentication failed");
sendResult(response,os,result);
return;
}
if( action.equals(ModelEntityEventAction.MODEL_REFRESH.getActionName()) ) {
logger.trace("Processing refresh");
result = refresh(request,response);
sendResult(response,os,result);
return;
} else if( action.equals(ClusterNodeRequest.HEALTH_CHECK) ) {
logger.trace("Cluster node health check");
result = healthCheck(request,response);
sendResult(response,os,result);
return;
}
GlobalSettings settings = modelManager.getGlobalSettings();
ClusterNode clusterNode = modelManager.getClusterNode();
if( clusterNode==null ) {
throw new RuntimeException("openmeap-services-web needs to be configured as a cluster node in the settings of the admin interface.");
}
Map<Method,String> validationErrors = clusterNode.validate();
if( validationErrors != null ) {
throw new RuntimeException(new InvalidPropertiesException(clusterNode,validationErrors));
}
if( request.getParameter("clearPersistenceContext")!=null && context instanceof AbstractApplicationContext ) {
logger.trace("Clearing persistence context");
clearPersistenceContext();
} else if( action.equals(ModelEntityEventAction.ARCHIVE_UPLOAD.getActionName()) ) {
logger.trace("Processing archive upload - max file size: {}, storage path prefix: {}",settings.getMaxFileUploadSize(),clusterNode.getFileSystemStoragePathPrefix());
archiveUploadHandler.setFileSystemStoragePathPrefix(clusterNode.getFileSystemStoragePathPrefix());
Map<Object,Object> paramMap = ServletUtils.cloneParameterMap(settings.getMaxFileUploadSize(),clusterNode.getFileSystemStoragePathPrefix(),request);
result = handleArchiveEvent(archiveUploadHandler, new MapPayloadEvent(paramMap), paramMap);
} else if( action.equals(ModelEntityEventAction.ARCHIVE_DELETE.getActionName()) ) {
logger.trace("Processing archive delete - max file size: {}, storage path prefix: {}",settings.getMaxFileUploadSize(),clusterNode.getFileSystemStoragePathPrefix());
archiveDeleteHandler.setFileSystemStoragePathPrefix(clusterNode.getFileSystemStoragePathPrefix());
Map<Object,Object> paramMap = ServletUtils.cloneParameterMap(settings.getMaxFileUploadSize(),clusterNode.getFileSystemStoragePathPrefix(), request);
result = handleArchiveEvent(archiveDeleteHandler, new MapPayloadEvent(paramMap), paramMap);
}
sendResult(response,os,result);
}
private void sendResult(HttpServletResponse response, PrintWriter os, Result result) throws IOException {
try {
if( result.getStatus()!=Result.Status.SUCCESS ) {
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
JSONObject jsonResult = new JSONObjectBuilder().toJSON(result);
String stringResult = jsonResult.toString(3);
logger.debug("returning json result: {}",stringResult);
os.print(jsonResult);
} catch( JSONException jse ) {
throw new IOException(jse);
}
os.flush();
os.close();
}
private void clearPersistenceContext() {
logger.info("Clearing the persistence context");
ModelServiceImpl ms = (ModelServiceImpl)((AbstractApplicationContext)context).getBean("modelService");
ms.clearPersistenceContext();
}
@SuppressWarnings(value={ "rawtypes", "unchecked" })
private Result handleArchiveEvent(EventHandler eventHandler, Event event, Map<Object,Object> paramMap) throws IOException {
String hash = firstValue(UrlParamConstants.APPARCH_HASH,paramMap);
String hashType = firstValue(UrlParamConstants.APPARCH_HASH_ALG,paramMap);
logger.info("Received request archive upload notification {}:{}",hashType,hash);
Result result = null;
if( hash!=null && hashType!=null ) {
ApplicationArchive arch = new ApplicationArchive();
arch.setHash(hash);
arch.setHashAlgorithm(hashType);
try {
paramMap.put("archive",arch);
eventHandler.handle(event);
result = new Result(Result.Status.SUCCESS);
} catch(EventHandlingException che) {
String msg = "Exception occurred handing the ArchiveUploadEvent";
logger.error(msg,che);
result = new Result(Result.Status.FAILURE,msg);
}
} else {
String msg = "Either the hash("+hash+") or the hashType("+hashType+") was null. Both are needed to process an archive event";
logger.error(msg);
result = new Result(Result.Status.FAILURE,msg);
}
return result;
}
/**
* Handles the notification that this node should refresh some object from the database.
*
* @param os
* @param request
* @param response
* @throws IOException
*/
private Result refresh(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/javascript");
String refreshType = (String)request.getParameter("type");
String objectId = (String)request.getParameter("id");
Result result = null;
if( refreshType!=null && objectId!=null ) {
logger.info("Received request to refresh {} with id {}",refreshType,objectId);
try {
modelServiceRefreshHandler.handleRefresh(refreshType,objectId);
logger.info("Refresh for {} with id {} was successful",refreshType,objectId);
result = new Result(Result.Status.SUCCESS);
} catch( Exception e ) {
String msg = "Exception occurred refreshing "+refreshType+" with object id "+objectId;
logger.error(msg,e);
result = new Result(Result.Status.FAILURE,msg);
}
} else {
String msg = "Must specify refresh target class, object primary key, and authentication token.";
logger.error(msg,request);
result = new Result(Result.Status.FAILURE,msg);
}
return result;
}
/**
*
* @param request
* @param response
* @return
* @throws IOException
*/
private Result healthCheck(HttpServletRequest request, HttpServletResponse response) throws IOException {
String json = Utils.readInputStream(request.getInputStream(), FormConstants.CHAR_ENC_DEFAULT);
Result result = null;
try {
ClusterNodeRequest nodeRequest = (ClusterNodeRequest)new JSONObjectBuilder().fromJSON(new JSONObject(json), new ClusterNodeRequest());
Map<String,String> properties = (Map<String,String>)context.getBean("openmeapServicesWebPropertiesMap");
synchronized(properties) {
properties.put("clusterNodeUrlPrefix", nodeRequest.getClusterNode().getServiceWebUrlPrefix());
properties.put("fileSystemStoragePathPrefix", nodeRequest.getClusterNode().getFileSystemStoragePathPrefix());
}
result = new Result(Result.Status.SUCCESS);
} catch (JSONException e) {
result = new Result();
result.setStatus(Result.Status.FAILURE);
String msg = "Failed to parse health status check JSON - "+json;
logger.error(msg);
result.setMessage(msg);
}
return result;
}
/**
* Validates that the auth in a request passes validation
* @param arg0
* @return
*/
private Boolean authenticates(HttpServletRequest arg0) {
String authSalt = getAuthSalt();
String auth = (String)arg0.getParameter(UrlParamConstants.AUTH_TOKEN);
Boolean isGood;
try {
isGood = AuthTokenProvider.validateAuthToken(authSalt, auth);
} catch (DigestException e) {
throw new GenericRuntimeException(e);
}
logger.debug("Authentication of token \"{}\" with salt \"{}\" returned {}",new Object[]{authSalt,auth,isGood});
return (auth!=null && isGood);
}
// ACCESSORS
public String getAuthSalt() {
GlobalSettings settings = modelManager.getGlobalSettings();
return settings.getServiceManagementAuthSalt();
}
public void setModelManager(ModelManager manager) {
modelManager = manager;
}
public ModelManager getModelManager() {
return modelManager;
}
public void setModelServiceRefreshHandler(ModelServiceRefreshHandler refreshHandler) {
modelServiceRefreshHandler = refreshHandler;
}
public ModelServiceRefreshHandler getModelServiceRefreshHandler() {
return modelServiceRefreshHandler;
}
}