package com.eas.server.httpservlet; import com.eas.client.Application; import com.eas.client.ClientConstants; import com.eas.client.DatabasesClient; import com.eas.client.LocalModulesProxy; import com.eas.client.ScriptedDatabasesClient; import com.eas.client.SqlQuery; import com.eas.client.cache.ApplicationSourceIndexer; import com.eas.client.cache.ModelsDocuments; import com.eas.client.cache.ScriptDocument; import com.eas.client.cache.ScriptsConfigs; import com.eas.client.login.AnonymousPlatypusPrincipal; import com.eas.client.login.PlatypusPrincipal; import com.eas.client.queries.LocalQueriesProxy; import com.eas.client.queries.QueriesProxy; import com.eas.client.scripts.ScriptedResource; import com.eas.client.threetier.Request; import com.eas.client.threetier.Requests; import com.eas.client.threetier.Response; import com.eas.client.threetier.http.PlatypusHttpRequestParams; import com.eas.client.threetier.requests.*; import com.eas.concurrent.PlatypusThreadFactory; import com.eas.script.JsObjectException; import com.eas.script.Scripts; import com.eas.server.*; import com.eas.util.IdGenerator; import com.eas.util.JsonUtils; import java.io.*; import java.nio.file.Path; import java.nio.file.Paths; import java.security.AccessControlException; import java.security.Principal; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.AsyncContext; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.Part; /** * Platypus HTTP servlet implementation * * @author ml, kl, mg refactoring */ public class PlatypusHttpServlet extends HttpServlet { public static final String CORE_MISSING_MSG = "Application core havn't been initialized"; public static final String SESSION_MISSING_MSG = "Session %s missing"; public static final String HTTP_SESSION_MISSING_MSG = "Container's session missing"; public static final String PLATYPUS_SESSION_MISSING_MSG = "Platypus session missing"; public static final String ERRORRESPONSE_ERROR_MSG = "Error while sending ErrorResponse"; public static final String UNKNOWN_REQUEST_MSG = "Unknown http request has arrived. It's type is %d"; public static final String REQUEST_PROCESSSING_ERROR_MSG = "Request processsing error"; public static final String SUBJECT_CONTEXT_KEY = "javax.security.auth.Subject.container"; public static final String HTTP_HOST_OBJECT_NAME = "http"; public static final String EXCEL_CONTENT_TYPE = "application/xls"; public static final String EXCELX_CONTENT_TYPE = "application/xlsx"; public static final String HTML_CONTENT_TYPE = "text/html"; public static final String TEXT_CONTENT_TYPE = "text/plain"; public static final String PLATYPUS_SESSION_ID_ATTR_NAME = "platypus-session-id"; public static final String PLATYPUS_USER_CONTEXT_ATTR_NAME = "platypus-user-context"; private static volatile PlatypusServerCore platypusCore; private String realRootPathName; private PlatypusServerConfig platypusConfig; private RestPointsScanner restScanner; private ExecutorService containerExecutor; private ExecutorService selfExecutor; @Override public void init(ServletConfig config) throws ServletException { try { super.init(config); realRootPathName = config.getServletContext().getRealPath("/"); platypusConfig = PlatypusServerConfig.parse(config); try { containerExecutor = (ExecutorService) InitialContext.doLookup("java:comp/DefaultManagedExecutorService"); } catch (NamingException ex) { try { containerExecutor = (ExecutorService) InitialContext.doLookup("java:comp/env/concurrent/ThreadPool"); } catch (NamingException ex1) { selfExecutor = new ThreadPoolExecutor(platypusConfig.getMaximumLpcThreads(), platypusConfig.getMaximumLpcThreads(), 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(platypusConfig.getMaximumLpcQueueSize()), new PlatypusThreadFactory("platypus-worker-", false)); ((ThreadPoolExecutor) selfExecutor).allowCoreThreadTimeOut(true); } } File realRoot = new File(realRootPathName); if (realRoot.exists() && realRoot.isDirectory()) { ScriptsConfigs lsecurityConfigs = new ScriptsConfigs(); final ServerTasksScanner tasksScanner = new ServerTasksScanner(); restScanner = new RestPointsScanner(); Path projectRoot = Paths.get(realRoot.toURI()); Path appFolder = platypusConfig.getSourcePath() != null ? projectRoot.resolve(platypusConfig.getSourcePath()) : projectRoot; Path apiFolder = projectRoot.resolve("WEB-INF" + File.separator + "classes"); ApplicationSourceIndexer indexer = new ApplicationSourceIndexer(appFolder, apiFolder, lsecurityConfigs, (String aModuleName, ScriptDocument.ModuleDocument aModuleDocument, File aFile) -> { tasksScanner.moduleScanned(aModuleName, aModuleDocument, aFile); restScanner.moduleScanned(aModuleName, aModuleDocument, aFile); }); ScriptedDatabasesClient basesProxy = new ScriptedDatabasesClient(platypusConfig.getDefaultDatasourceName(), indexer, true, tasksScanner.getValidators(), platypusConfig.getMaximumJdbcThreads()); QueriesProxy<SqlQuery> queries = new LocalQueriesProxy(basesProxy, indexer); basesProxy.setQueries(queries); platypusCore = new PlatypusServerCore(indexer, new LocalModulesProxy(indexer, new ModelsDocuments(), platypusConfig.getAppElementName()), queries, basesProxy, lsecurityConfigs, platypusConfig.getAppElementName(), SessionManager.Singleton.instance, platypusConfig.getMaximumSpaces()) { @Override public Application.Type getType() { return Application.Type.SERVLET; } }; basesProxy.setContextHost(platypusCore); Scripts.initBIO(platypusConfig.getMaximumBIOTreads()); ScriptedResource.init(platypusCore, apiFolder, platypusConfig.isGlobalAPI()); Scripts.initTasks(containerExecutor != null ? containerExecutor /* J2EE 7+ */ : selfExecutor /* Other environment */); if (platypusConfig.isWatch()) { // TODO: Uncomment after watcher refactoring //indexer.watch(); } } else { throw new IllegalArgumentException("Application path: " + realRootPathName + " doesn't point to existent directory."); } } catch (Throwable ex) { throw new ServletException(ex); } } public static PlatypusServerCore getCore() { return platypusCore; } @Override public void destroy() { if (platypusConfig.isWatch()) { try { platypusCore.getIndexer().unwatch(); } catch (Exception ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); } } Scripts.shutdown(); if (platypusCore.getDatabasesClient() != null) { try { platypusCore.getDatabasesClient().shutdown(); } catch (InterruptedException ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); } } if (selfExecutor != null) { selfExecutor.shutdown(); try { selfExecutor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); } } super.destroy(); } protected static final String PUB_CONTEXT = "/pub/"; protected boolean checkUpload(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (request.getContentType() != null && request.getContentType().contains("multipart/form-data")) { List<StringBuilder> uploadedLocations = new ArrayList(); for (Part part : request.getParts()) { String dispositionHeader = part.getHeader("content-disposition"); if (dispositionHeader != null) { Pattern fileNamePattern = Pattern.compile(".*filename=.*\"(.+)\".*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); Matcher m = fileNamePattern.matcher(dispositionHeader); String fileName = null; if (m.matches()) { fileName = m.group(1); } if (fileName != null && !fileName.isEmpty()) { StringBuilder uploadedFileName = new StringBuilder(); uploadedFileName.append(IdGenerator.genId()).append("-").append(fileName); try { try { part.write(uploadedFileName.toString()); } catch (IOException ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, "Falling back to copy implementation", ex); String realPath = request.getServletContext().getRealPath(PUB_CONTEXT + uploadedFileName.toString()); try (InputStream fin = part.getInputStream(); FileOutputStream fout = new FileOutputStream(realPath)) { byte[] buffer = new byte[1024 * 16]; int read = fin.read(buffer); while (read >= 0) { fout.write(buffer, 0, read); read = fin.read(buffer); } } } } finally { part.delete(); } StringBuilder uploadedLocation = new StringBuilder(); uploadedLocation.append("http://").append(request.getHeader("host")).append(request.getServletContext().getContextPath()).append(PUB_CONTEXT).append(uploadedFileName); uploadedLocations.add(uploadedLocation); } } } PlatypusHttpResponseWriter.writeJsonResponse(JsonUtils.as(uploadedLocations.toArray(new StringBuilder[]{})).toString(), response, null); return true; } else { return false; } } /** * Processes requests for both HTTP <code>GET</code> and <code>POST</code> * methods. * * @param request servlet request * @param response servlet response * @throws ServletException if a servlet-specific error occurs * @throws IOException if an I/O error occurs */ protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { if (!checkUpload(request, response)) { if (platypusCore != null) { HttpSession httpSession = request.getSession(); if (httpSession != null) { AsyncContext async = request.startAsync(); async.setTimeout(-1); String userName = request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null; Consumer<Session> withPlatypusSession = (Session aSession) -> { // http executor thread or sessions accounting thread // temporarily session thread try { handlePlatypusRequest(request, response, httpSession, aSession, async); } catch (Exception ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.toString()); async.complete(); } catch (IOException | IllegalStateException ex1) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex1); } } }; Session lookedup0 = platypusSessionByHttpSession(httpSession); if (lookedup0 == null) {// Zombie check platypusCore.getQueueSpace().process(() -> { // sessions accounting thread Session lookedup1 = platypusSessionByHttpSession(httpSession); if (lookedup1 == null) { try { Consumer<String> withDataContext = (String dataContext) -> { String platypusSessionId = (String) httpSession.getAttribute(PLATYPUS_SESSION_ID_ATTR_NAME); // platypusSessionId may be replicated from another instance in cluster Session lookedup2 = platypusSessionId != null ? SessionManager.Singleton.instance.get(platypusSessionId) : null; if (lookedup2 == null) { try { // preserve replicated session id Session created = SessionManager.Singleton.instance.create(platypusSessionId == null ? IdGenerator.genId() + "" : platypusSessionId); if (dataContext == null) { httpSession.removeAttribute(PLATYPUS_USER_CONTEXT_ATTR_NAME); } else { httpSession.setAttribute(PLATYPUS_USER_CONTEXT_ATTR_NAME, dataContext); } // publishing a session httpSession.setAttribute(PLATYPUS_SESSION_ID_ATTR_NAME, created.getId()); // a session has been published Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.INFO, "Http platypus session opened. Session id: {0}", created.getId()); withPlatypusSession.accept(created); } catch (Exception ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.toString()); async.complete(); } catch (IOException | IllegalStateException ex1) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex1); } } } else { withPlatypusSession.accept(lookedup2); } }; if (request.getUserPrincipal() != null) {// Additional properties can be obtained only for authorized users DatabasesClient.getUserProperties(platypusCore.getDatabasesClient(), userName, platypusCore.getQueueSpace(), (Map<String, String> aUserProps) -> { // still sessions accounting thread String dataContext = aUserProps.get(ClientConstants.F_USR_CONTEXT); withDataContext.accept(dataContext); }, (Exception ex) -> { // still sessions accounting thread Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.FINE, "Unable to obtain properties of user {0} due to an error: {1}", new Object[]{userName, ex.toString()}); withDataContext.accept(null); }); } else { withDataContext.accept(null); } } catch (Exception ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex); try { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.toString()); async.complete(); } catch (IOException | IllegalStateException ex1) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex1); } } } else { withPlatypusSession.accept(lookedup1); } }); } else { // http executor thread withPlatypusSession.accept(lookedup0); } } else { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, HTTP_SESSION_MISSING_MSG); } } else { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, CORE_MISSING_MSG); } } } public Session platypusSessionByHttpSession(HttpSession httpSession) { SessionManager sessionManager = platypusCore.getSessionManager(); String platypusSessionId = (String) httpSession.getAttribute(PLATYPUS_SESSION_ID_ATTR_NAME); Session session = platypusSessionId != null ? sessionManager.get(platypusSessionId) : null; return session; } private static PlatypusPrincipal httpRequestPrincipal(final HttpServletRequest aRequest) { HttpSession httpSession = aRequest.getSession(false); if (aRequest.getUserPrincipal() != null) { return new HttpPlatypusPrincipal(aRequest.getUserPrincipal().getName(), (String) httpSession.getAttribute(PLATYPUS_USER_CONTEXT_ATTR_NAME), aRequest); } else { return httpSession != null ? new AnonymousPlatypusPrincipal("anonymous-" + httpSession.getId()) : new AnonymousPlatypusPrincipal(); } } public PlatypusServerCore getServerCore() { return platypusCore; } /** * Precesses request for PlatypusAPI requests. * * @param aHttpRequest * @param aPlatypusSession * @param aHttpResponse * @param aHttpSession * @throws Exception */ private void handlePlatypusRequest(final HttpServletRequest aHttpRequest, final HttpServletResponse aHttpResponse, HttpSession aHttpSession, Session aPlatypusSession, AsyncContext aAsync) throws Exception { // temporarily session thread Request platypusRequest = readPlatypusRequest(aHttpRequest, aHttpResponse); if (platypusRequest.getType() == Requests.rqLogout) { aHttpRequest.logout(); aHttpSession.invalidate(); aHttpResponse.setStatus(HttpServletResponse.SC_OK); aAsync.complete(); } else { RequestHandler<Request, Response> handler = (RequestHandler<Request, Response>) RequestHandlerFactory.getHandler(platypusCore, platypusRequest); if (handler != null) { Consumer<Exception> onFailure = (Exception ex) -> { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, ex.toString()); try { if (ex instanceof AccessControlException) { /* // We can't send HttpServletResponse.SC_UNAUTHORIZED without knowlege about login mechanisms // of J2EE container. AccessControlException accEx = (AccessControlException)ex; aHttpResponse.sendError(accEx.getPermission() instanceof AuthPermission ? HttpServletResponse.SC_UNAUTHORIZED : HttpServletResponse.SC_FORBIDDEN, ex.getMessage()); */ aHttpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage()); } else if (ex instanceof FileNotFoundException) { aHttpResponse.sendError(HttpServletResponse.SC_NOT_FOUND, ex.getMessage()); } else if (ex instanceof JsObjectException) { String errorBody = aPlatypusSession.getSpace().toJson(((JsObjectException) ex).getData()); if (aHttpResponse.getStatus() >= 200 && aHttpResponse.getStatus() < 300) { aHttpResponse.setStatus(HttpServletResponse.SC_CONFLICT); } PlatypusHttpResponseWriter.writeJsonResponse(errorBody, aHttpResponse, null); } else { aHttpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); } aAsync.complete(); } catch (IOException ex1) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, null, ex1); } }; aPlatypusSession.accessed(); Scripts.LocalContext context = new Scripts.LocalContext(aHttpRequest, aHttpResponse, httpRequestPrincipal(aHttpRequest), aPlatypusSession); aPlatypusSession.getSpace().process(context, () -> { handler.handle(aPlatypusSession, (Response resp) -> { assert Scripts.getSpace() == aPlatypusSession.getSpace(); PlatypusHttpResponseWriter writer = new PlatypusHttpResponseWriter(aHttpResponse, aHttpRequest, aPlatypusSession.getSpace(), ((Principal) Scripts.getContext().getPrincipal()).getName(), aAsync); try { resp.accept(writer); } catch (Exception ex) { Logger.getLogger(PlatypusHttpServlet.class.getName()).log(Level.SEVERE, ex.getMessage()); } }, (Exception ex) -> { onFailure.accept(ex); }); }); } else { throw new IllegalStateException("No request handler found"); } } } // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code."> @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { processRequest(request, response); } catch (Exception ex) { throw new ServletException(ex); } } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { processRequest(req, resp); } catch (Exception ex) { throw new ServletException(ex); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { processRequest(req, resp); } catch (Exception ex) { throw new ServletException(ex); } } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { processRequest(req, resp); } catch (Exception ex) { throw new ServletException(ex); } } /** * Returns a short description of the servlet. * * @return a String containing servlet description */ @Override public String getServletInfo() { return "Platypus servlet provides platypus server functionality within a J2EE/Servlet container"; }// </editor-fold> protected Request readPlatypusRequest(HttpServletRequest aHttpRequest, HttpServletResponse aResponse) throws Exception { String sType = aHttpRequest.getParameter(PlatypusHttpRequestParams.TYPE); if (sType == null && aHttpRequest.getParameter(PlatypusHttpRequestParams.MODULE_NAME) != null && aHttpRequest.getParameter(PlatypusHttpRequestParams.METHOD_NAME) != null) { sType = "" + Requests.rqExecuteServerModuleMethod; } if (sType != null) { int rqType = Integer.valueOf(sType); Request rq = PlatypusRequestsFactory.create(rqType); if (rq != null) { PlatypusHttpRequestReader reader = new PlatypusHttpRequestReader(platypusCore, aHttpRequest); rq.accept(reader); return rq; } else { throw new Exception(String.format(UNKNOWN_REQUEST_MSG, rqType)); } } else { String contextedUri = aHttpRequest.getPathInfo(); if (contextedUri != null) { Map<String, RPCPoint> methoded = restScanner.getMethoded().get(aHttpRequest.getMethod().toLowerCase()); if (methoded != null) { String contextedUriHead = contextedUri; while (!contextedUriHead.isEmpty() && !methoded.containsKey(contextedUriHead)) { contextedUriHead = contextedUriHead.substring(0, contextedUriHead.lastIndexOf("/")); } if (methoded.containsKey(contextedUriHead)) { RPCPoint rpcPoint = methoded.get(contextedUriHead); String tail = contextedUri.substring(contextedUriHead.length()); if (tail.startsWith("/")) { tail = tail.substring(1); } if (tail.endsWith("/")) { tail = tail.substring(0, tail.length() - 1); } return new RPCRequest(rpcPoint.getModuleName(), rpcPoint.getMethodName(), new String[]{JsonUtils.s(!tail.isEmpty() ? tail : "").toString()}); } } } throw new Exception(String.format("Neither REST endpoint for URI %s, nor API parameters ('%s', '%s', '%s', etc.) found in the request", contextedUri, PlatypusHttpRequestParams.TYPE, PlatypusHttpRequestParams.QUERY_ID, PlatypusHttpRequestParams.MODULE_NAME)); } } }