/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.server.ngclient; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import org.sablo.eventthread.IEventDispatcher; import org.sablo.specification.WebObjectSpecification; import org.sablo.specification.WebServiceSpecProvider; import org.sablo.websocket.BaseWebsocketSession; import org.sablo.websocket.CurrentWindow; import org.sablo.websocket.IClientService; import org.sablo.websocket.IServerService; import org.sablo.websocket.IWindow; import org.sablo.websocket.WebsocketSessionManager; import com.servoy.j2db.FlattenedSolution; import com.servoy.j2db.J2DBGlobals; import com.servoy.j2db.Messages; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.Solution; import com.servoy.j2db.scripting.StartupArguments; import com.servoy.j2db.server.ngclient.eventthread.NGClientWebsocketSessionWindows; import com.servoy.j2db.server.ngclient.eventthread.NGEventDispatcher; import com.servoy.j2db.server.shared.ApplicationServerRegistry; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.PersistHelper; import com.servoy.j2db.util.Settings; import com.servoy.j2db.util.Utils; /** * Handles a websocket session based on a NGClient. * * @author rgansevles * */ public class NGClientWebsocketSession extends BaseWebsocketSession implements INGClientWebsocketSession { private NGClient client; public NGClientWebsocketSession(String uuid) { super(uuid); } @Override public void init() throws Exception { if (client == null) { setClient(new NGClient(this)); } } public void setClient(NGClient client) { this.client = client; } public NGClient getClient() { return client; } @Override public INGClientWindow createWindow(String windowUuid, String windowName) { return new NGClientWindow(this, windowUuid, windowName); } @SuppressWarnings("unchecked") @Override public Collection<INGClientWindow> getWindows() { return (Collection<INGClientWindow>)super.getWindows(); } @Override public boolean isValid() { return client != null && !client.isShutDown(); } @Override protected IEventDispatcher createEventDispatcher() { return new NGEventDispatcher(client); } @Override public void onOpen(final Map<String, List<String>> requestParams) { super.onOpen(requestParams); if (requestParams == null) { CurrentWindow.get().cancelSession("Solution name is required"); return; } final StartupArguments args = new StartupArguments(requestParams); final String solutionName = args.getSolutionName(); if (Utils.stringIsEmpty(solutionName)) { CurrentWindow.get().cancelSession("Invalid solution name"); return; } if (!client.isEventDispatchThread()) J2DBGlobals.setServiceProvider(client); try { FlattenedSolution solution = client.getFlattenedSolution(); if (solution != null) { // test for the main solution meta data else a login solution will constantly be closed even if it is for the right main solution. if (solution.getSolution() != null && !solutionName.equals(solution.getMainSolutionMetaData().getName())) { client.closeSolution(true, null); } else { if (solution.isMainSolutionLoaded()) { //this is needed for the situation when the solution is already loaded and the deeplink url was changed (different arg values for instance) String method = args.getMethodName(); String firstArgument = args.getFirstArgument(); if (method != null) { try { client.getScriptEngine().getScopesScope().executeGlobalFunction(null, method, (firstArgument == null ? null : new Object[] { firstArgument, args.toJSMap() }), false, false); } catch (Exception e1) { client.reportError(Messages.getString("servoy.formManager.error.ExecutingOpenSolutionMethod", new Object[] { method }), e1); //$NON-NLS-1$ } } } client.getRuntimeWindowManager().setCurrentWindowName(CurrentWindow.get().getUuid()); IWebFormController currentForm = client.getFormManager().getCurrentForm(); if (currentForm != null) { // we have to call setcontroller again so that switchForm is called and the form is loaded into the reloaded/new window. startHandlingEvent(); try { client.getRuntimeWindowManager().getCurrentWindow().setController(currentForm); sendSolutionCSSURL(solution.getSolution()); } finally { stopHandlingEvent(); } return; } } } getEventDispatcher().addEvent(new Runnable() { @Override public void run() { try { // the solution was not loaded or another was loaded, now create a main window and load the solution. client.getRuntimeWindowManager().createMainWindow(CurrentWindow.get().getUuid()); client.handleArguments( args.getFirstArgument() != null ? new String[] { args.getSolutionName(), args.getMethodName(), args.getFirstArgument() } : new String[] { args.getSolutionName(), args.getMethodName() }, args); client.loadSolution(solutionName); } catch (RepositoryException e) { Debug.error("Failed to load the solution: " + solutionName, e); sendInternalError(e); } } }); } catch (Exception e) { Debug.error(e); sendInternalError(e); } finally { if (!client.isEventDispatchThread()) J2DBGlobals.setServiceProvider(null); } } @Override protected IServerService createFormService() { return new NGFormServiceHandler(this); } @Override public void solutionLoaded(Solution solution) { sendSolutionCSSURL(solution); } public void sendStyleSheet() { if (client != null) sendSolutionCSSURL(client.getSolution()); } protected void sendSolutionCSSURL(Solution solution) { Map<String, String> overrideStyleSheets = client != null ? client.getOverrideStyleSheets() : null; List<String> styleSheets = PersistHelper.getOrderedStyleSheets(client.getFlattenedSolution()); if (styleSheets != null && styleSheets.size() > 0) { if (overrideStyleSheets != null) { for (String oldStyleSheet : overrideStyleSheets.keySet()) { if (styleSheets.contains(oldStyleSheet)) { styleSheets.set(styleSheets.indexOf(oldStyleSheet), overrideStyleSheets.get(oldStyleSheet)); } } } Collections.reverse(styleSheets); for (int i = 0; i < styleSheets.size(); i++) { styleSheets.set(i, "resources/" + MediaResourcesServlet.FLATTENED_SOLUTION_ACCESS + "/" + solution.getName() + "/" + styleSheets.get(i)); } getClientService(NGClient.APPLICATION_SERVICE).executeAsyncServiceCall("setStyleSheets", new Object[] { styleSheets.toArray(new String[0]) }); } else { getClientService(NGClient.APPLICATION_SERVICE).executeAsyncServiceCall("setStyleSheets", new Object[] { }); } } @Override public void valueChanged() { if (client != null) { super.valueChanged(); } } public void sendRedirect(final String redirectUrl) { IWindow curr = CurrentWindow.safeGet(); CurrentWindow.runForWindow(curr != null && redirectUrl != null ? curr : new NGClientWebsocketSessionWindows(client.getWebsocketSession()), new Runnable() { @Override public void run() { Map<String, Object> detail = new HashMap<>(); String htmlfilePath = Settings.getInstance().getProperty("servoy.webclient.pageexpired.page"); if (htmlfilePath != null) detail.put("viewUrl", htmlfilePath); if (redirectUrl != null) detail.put("redirectUrl", redirectUrl); getClientService("$sessionService").executeAsyncServiceCall("expireSession", new Object[] { detail }); } }); } @Override protected IClientService createClientService(String name) { WebObjectSpecification spec = WebServiceSpecProvider.getInstance().getWebServiceSpecification(name); if (spec == null) spec = new WebObjectSpecification(name, "", name, null, null, null, "", null); return new ServoyClientService(name, spec, this); } /* * All windows are now closed. We shutdown the client in order to free up the license/resources for the next NGClient instantiation. * * @see org.sablo.websocket.BaseWebsocketSession#sessionExpired() */ @Override public void sessionExpired() { getClient().shutDown(true); super.sessionExpired(); } /** * Sets an internalServerError object on the client side which shows the internal server error page. * If it is run from the developer it also adds the stack trace * @param e */ public static void sendInternalError(Exception e) { Map<String, Object> internalError = new HashMap<>(); StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); String stackTrace = sw.toString(); if (ApplicationServerRegistry.get().isDeveloperStartup()) internalError.put("stack", stackTrace); String htmlView = Settings.getInstance().getProperty("servoy.webclient.error.page"); if (htmlView != null) internalError.put("viewUrl", htmlView); CurrentWindow.get().getSession().getClientService("$sessionService").executeAsyncServiceCall("setInternalServerError", new Object[] { internalError }); } @Override public void updateLastAccessed(IWindow window) { super.updateLastAccessed(window); // check for window activity each time a window is closed, after the timeout period ApplicationServerRegistry.get().getExecutor().schedule(new Runnable() { @Override public void run() { WebsocketSessionManager.closeInactiveSessions(); } }, getWindowTimeout() + 10, TimeUnit.MILLISECONDS); } @Override public Locale getLocale() { return client.getLocale(); } @Override public INGClientWindow getWindowWithForm(String formName) { for (INGClientWindow w : getWindows()) { if (w.hasForm(formName)) return w; } return null; } }