/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 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.headlessclient; import java.awt.Dimension; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.swing.SwingUtilities; import org.apache.wicket.AbortException; import org.apache.wicket.Application; import org.apache.wicket.PageParameters; import org.apache.wicket.RequestCycle; import org.apache.wicket.RestartResponseException; import org.apache.wicket.Session; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.protocol.http.ClientProperties; import org.apache.wicket.protocol.http.WebRequest; import org.apache.wicket.protocol.http.WebRequestCycle; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.protocol.http.request.WebClientInfo; import org.apache.wicket.request.target.basic.RedirectRequestTarget; import com.servoy.j2db.ApplicationException; import com.servoy.j2db.FormManager; import com.servoy.j2db.IFormManagerInternal; import com.servoy.j2db.IServiceProvider; import com.servoy.j2db.IWebClientApplication; import com.servoy.j2db.RuntimeWindowManager; import com.servoy.j2db.component.ComponentFactory; import com.servoy.j2db.dataprocessing.ClientInfo; import com.servoy.j2db.dataprocessing.IUserClient; import com.servoy.j2db.persistence.Solution; import com.servoy.j2db.persistence.Style; import com.servoy.j2db.plugins.IClientPluginAccess; import com.servoy.j2db.scripting.IScriptSupport; import com.servoy.j2db.scripting.info.WEBCONSTANTS; import com.servoy.j2db.server.headlessclient.MainPage.ShowUrlInfo; import com.servoy.j2db.server.headlessclient.ServoyBrowserInfoPage.ServoyWebClientInfo; import com.servoy.j2db.server.headlessclient.eventthread.IEventDispatcher; import com.servoy.j2db.server.headlessclient.eventthread.WicketEvent; import com.servoy.j2db.server.headlessclient.eventthread.WicketEventDispatcher; import com.servoy.j2db.server.shared.WebCredentials; import com.servoy.j2db.util.Ad; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.ServoyException; import com.servoy.j2db.util.Settings; import com.servoy.j2db.util.Utils; /** * A client which uses the org.apache.wicket framework to render a GUI in a web browser * * @author jcompagner */ public class WebClient extends SessionClient implements IWebClientApplication { private static final String COOKIE_BASE64_PREFIX = "B64p_"; private Map<Object, Object> uiProperties; protected WebClient(HttpServletRequest req, WebCredentials credentials, String method, Object[] methodArgs, String solution) throws Exception { super(req, credentials, method, methodArgs, solution); //set the remote info, since localhost from server is useless ClientInfo ci = getClientInfo(); String ipaddr = req.getHeader("X-Forwarded-For");//incase there is a forwarding proxy //$NON-NLS-1$ if (ipaddr == null) { ipaddr = req.getRemoteAddr(); } ci.setHostAddress(ipaddr); ci.setHostName(req.getRemoteHost()); ci.setTimeZone(getTimeZone()); // getTimeZone() must always be called here to cache the time-zone in the beginning, because otherwise scheduler tasks may fail (when performing find for example) - if executed before the first // call to getTimeZone() from within a browser request } @Override public int getApplicationType() { return WEB_CLIENT; } @Override protected RuntimeWindowManager createJSWindowManager() { return new WebRuntimeWindowManager(this); } @SuppressWarnings("nls") @Override public URL getServerURL() { if (Session.exists()) { Session webClientSession = Session.get(); if (webClientSession != null) { WebClientInfo clientInfo = (WebClientInfo)webClientSession.getClientInfo(); if (clientInfo != null && clientInfo.getProperties() != null && clientInfo.getProperties().getHostname() != null) { String hostname = clientInfo.getProperties().getHostname(); try { if (hostname.startsWith("http")) { // first try to find the wicket servlet int index = hostname.indexOf("/servoy-webclient", 8); //8 is to skip http:// or https:// // if that fails then just try to find the first / if (index == -1) index = hostname.indexOf('/', 8); //8 is to skip http:// or https:// if (index == -1) { return new URL(hostname); } else { return new URL(hostname.substring(0, index)); } } else { return new URL("http", hostname, ""); } } catch (MalformedURLException e) { Debug.error(e); } } } } return super.getServerURL(); } @Override @SuppressWarnings("nls") public String getClientOSName() { if (Session.exists()) { Session webClientSession = Session.get(); if (webClientSession != null) { WebClientInfo clientInfo = (WebClientInfo)webClientSession.getClientInfo(); if (clientInfo != null && clientInfo.getProperties() != null) { String userAgent = clientInfo.getUserAgent(); if (userAgent != null) { if (userAgent.indexOf("NT 6.1") != -1) return "Windows 7"; if (userAgent.indexOf("NT 6.0") != -1) return "Windows Vista"; if (userAgent.indexOf("NT 5.1") != -1 || userAgent.indexOf("Windows XP") != -1) return "Windows XP"; if (userAgent.indexOf("Linux") != -1) return "Linux"; if (userAgent.indexOf("Mac") != -1) return "Mac OS"; } return clientInfo.getProperties().getNavigatorPlatform(); } } } return System.getProperty("os.name"); } @Override public int getClientPlatform() { if (Session.exists()) { Session webClientSession = Session.get(); if (webClientSession != null) { try { WebClientInfo clientInfo = (WebClientInfo)webClientSession.getClientInfo(); if (clientInfo != null) { ClientProperties properties = clientInfo.getProperties(); if (properties != null) { return Utils.getPlatform(properties.getNavigatorPlatform()); } } } catch (Exception e) { Debug.trace("trying to get the client platform of a session, when destroying the client in a none request thread", e); } } } return super.getClientPlatform(); } @Override public TimeZone getTimeZone() { if (timeZone == null && Session.exists()) { WebClientSession webClientSession = ((WebClientSession)Session.get()); timeZone = ((WebClientInfo)webClientSession.getClientInfo()).getProperties().getTimeZone(); // if the timezone is really just the default of the server just use that one. TimeZone dftZone = TimeZone.getDefault(); if (timeZone.getRawOffset() == dftZone.getRawOffset() && timeZone.getDSTSavings() == dftZone.getDSTSavings()) { timeZone = dftZone; } } return super.getTimeZone(); } private final Map<String, String> userRequestProperties = new HashMap<String, String>(); /** * @see com.servoy.j2db.server.headlessclient.SessionClient#getUserProperty(java.lang.String) */ @Override public String getUserProperty(String name) { if (name == null) return null; // first look if there is a property that was set in the same request. String value = userRequestProperties.get(name); if (value != null) return value; String defaultUserProperty = getDefaultUserProperties().get(name); if (defaultUserProperty != null) return defaultUserProperty; WebRequestCycle wrc = ((WebRequestCycle)RequestCycle.get()); if (wrc != null && wrc.getWebRequest() != null) { Cookie[] cookies = wrc.getWebRequest().getCookies(); if (cookies != null) { for (Cookie element : cookies) { if (name.equals(element.getName())) { return decodeCookieValue(element.getValue()); } } } } return null; } @Override public String[] getUserPropertyNames() { List<String> retval = new ArrayList<String>(); WebRequestCycle wrc = ((WebRequestCycle)RequestCycle.get()); if (wrc != null && wrc.getWebRequest() != null) { Cookie[] cookies = wrc.getWebRequest().getCookies(); for (Cookie element : cookies) { retval.add(element.getName()); } } for (String defaultUserPropertyKey : getDefaultUserProperties().keySet()) { if (retval.indexOf(defaultUserPropertyKey) == -1) { retval.add(defaultUserPropertyKey); } } return retval.toArray(new String[retval.size()]); } /** * @see com.servoy.j2db.server.headlessclient.SessionClient#setUserProperty(java.lang.String, java.lang.String) */ @SuppressWarnings("nls") @Override public void setUserProperty(String name, String value) { if (name == null) return; if (RequestCycle.get() == null) { Debug.log("Set user property called when there is no request (onload of a from with a session timeout?), property " + name + ", value " + value + " not saved"); return; } getDefaultUserProperties().remove(name); // first set in the the special request properties map if (value == null) { userRequestProperties.remove(name); } else { userRequestProperties.put(name, value); } WebRequest webRequest = ((WebRequestCycle)RequestCycle.get()).getWebRequest(); // calculate the base path (servlet path) // it can be /path/ or /context/path/ or even just / if it is virtual hosted so try to get it from the url. String url = webRequest.getHttpServletRequest().getRequestURL().toString(); // first try to get to the first / of the root path, strip off http://domain:port int index = url.indexOf("//"); if (index > 0) { url = url.substring(index + 2); } index = url.indexOf('/'); if (index > 0) { url = url.substring(index); } else { url = "/"; } String path = webRequest.getPath(); if (path.length() == 0) path = "?"; index = url.indexOf(path); if (index > 0) { path = url.substring(0, index); } else { path = url; } if (!path.startsWith("/")) path = "/" + path; Cookie[] cookies = webRequest.getCookies(); if (cookies != null) { for (Cookie element : cookies) { if (name.equals(element.getName())) { element.setPath(path); ((WebRequestCycle)RequestCycle.get()).getWebResponse().clearCookie(element); break; } } } if (value != null) { Cookie cookie = new Cookie(name, encodeCookieValue(value)); cookie.setMaxAge(Integer.MAX_VALUE); cookie.setPath(path); // when in secure request, browser does not send cookie over insecure request cookie.setSecure(Boolean.parseBoolean(Settings.getInstance().getProperty("servoy.webclient.enforceSecureCookies", "false")) || ((WebRequestCycle)RequestCycle.get()).getWebRequest().getHttpServletRequest().isSecure()); // Add the cookie ((WebRequestCycle)RequestCycle.get()).getWebResponse().addCookie(cookie); } } /** * Reads a cookie, and decodes it if it was stored in Base64 using {@link #encodeCookieValue(String)}. * @param value the cookie contents. * @return the useful cookie contents. */ public static String decodeCookieValue(String value) { String cookieValue = value; if (cookieValue != null && cookieValue.startsWith(COOKIE_BASE64_PREFIX)) { try { cookieValue = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(Utils.decodeBASE64(cookieValue.substring(COOKIE_BASE64_PREFIX.length()))), "UTF-8")).readLine(); } catch (UnsupportedEncodingException e) { Debug.error(e); } catch (IOException e) { Debug.error(e); } } return cookieValue; } /** * Encodes a value into Base64 so that it can be stored in a cookie without special character problems. * @param value the useful value that needs to be encoded. * @return the encoded value that can be decoded using {@link #decodeCookieValue(String)}. */ public static String encodeCookieValue(String value) { try { return COOKIE_BASE64_PREFIX + Utils.encodeBASE64(value.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { Debug.error(e); return value; } } @Override public boolean putClientProperty(Object name, Object val) { if (WEBCONSTANTS.WEBCLIENT_TEMPLATES_DIR.equals(name)) { WebClientSession webClientSession = WebClientSession.get(); if (webClientSession != null) webClientSession.setTemplateDirectoryName((val == null ? null : val.toString())); } else { if (uiProperties == null) { uiProperties = new HashMap<Object, Object>(); } uiProperties.put(name, val); } return true; } @Override public Object getClientProperty(Object name) { if (WEBCONSTANTS.WEBCLIENT_TEMPLATES_DIR.equals(name)) { WebClientSession webClientSession = WebClientSession.get(); return webClientSession == null ? "" : webClientSession.getTemplateDirectoryName(); } else { return (uiProperties == null) ? null : uiProperties.get(name); } } private transient Object[] adsInfo = null;//chache to expensive to get each time @Override protected boolean registerClient(IUserClient uc) throws Exception { boolean registered = false; try { registered = super.registerClient(uc); if (!registered) { if (adsInfo == null) adsInfo = Ad.getAdInfo(); final int w = Utils.getAsInteger(adsInfo[1]); final int h = Utils.getAsInteger(adsInfo[2]); if (w > 50 && h > 50) { URL url = (URL)adsInfo[0]; int t = Utils.getAsInteger(adsInfo[3]); getMainPage().getPageContributor().addDynamicJavaScript( "showInfoPanel('" + url + "'," + w + ',' + h + ',' + t + ",'" + getI18NMessage("servoy.button.close") + "');"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ } } } catch (final ApplicationException e) { if (e.getErrorCode() == ServoyException.NO_LICENSE) { throw new RestartResponseException(ServoyServerToBusyPage.class); } else if (e.getErrorCode() == ServoyException.MAINTENANCE_MODE) { throw new RestartResponseException(ServoyServerInMaintenanceMode.class); } } return registered; } private boolean shuttingDown = false; /** * @see com.servoy.j2db.server.headlessclient.SessionClient#isEventDispatchThread() */ @Override public boolean isEventDispatchThread() { // just execute everything immediately if client is shutting down if (isShutDown()) return true; // We test here for printing, WebForm.processFppInAWTEventQueue(..) will call SwingUtilities.invokeAndWait() to print in awt thread. return RequestCycle.get() != null || SwingUtilities.isEventDispatchThread(); } private final List<Runnable> events = new ArrayList<Runnable>(); private final static ThreadLocal<List<Runnable>> requestEvents = new ThreadLocal<List<Runnable>>() { @Override protected java.util.List<Runnable> initialValue() { return new ArrayList<Runnable>(); } }; private boolean blockEventExecution; boolean blockEventExecution(boolean block) { boolean prev = this.blockEventExecution; this.blockEventExecution = block; return prev; } @SuppressWarnings("nls") public void executeEvents() { if (blockEventExecution) return; List<Runnable> runnables = null; // This can get called during constructor, if an exception is thrown from super(), through shutdown(), // so the events array may be not initialized yet. if (events != null) { synchronized (events) { if (events.size() > 0) { runnables = new ArrayList<Runnable>(events); events.clear(); } } List<Runnable> list = requestEvents.get(); if (list.size() > 0) { if (runnables == null) { runnables = new ArrayList<Runnable>(list); } else runnables.addAll(list); list.clear(); } } if (runnables != null) { if (getEventDispatcher() != null) { getEventDispatcher().addEvent(new WicketEvent(this, new EventsRunnable(runnables))); } else { new EventsRunnable(this, runnables).run(); } } return; } /** * returns the request/thread local events, clears them when there are events waiting * So the caller must execute them. * @return */ public List<Runnable> getRequestEvents() { List<Runnable> lst = requestEvents.get(); if (lst.size() > 0) { ArrayList<Runnable> copy = new ArrayList<Runnable>(lst); lst.clear(); return copy; } return lst; } /** * @see com.servoy.j2db.server.headlessclient.SessionClient#invokeAndWait(java.lang.Runnable) */ @Override public void invokeAndWait(Runnable r) { if (isEventDispatchThread()) { // we have to test for the thread locals because isEventDispatchThread can return true for 2 threads including AWT Thread IServiceProvider prev = testThreadLocals(); try { r.run(); } finally { unsetThreadLocals(prev); } } else { synchronized (events) { events.add(r); } synchronized (r) { try { r.wait(); } catch (InterruptedException e) { Debug.error(e); } } } } /** * @see com.servoy.j2db.server.headlessclient.SessionClient#invokeLater(java.lang.Runnable) */ @Override protected void doInvokeLater(Runnable r) { // When shutting down call runnable immediately, otherwise it may never happen. // When printing (SwingUtilities.isEventDispatchThread()) runnable also needs to be called directly. // In all other cases the events will be called from executeEvents(), see WebClientsApplication.processEvents(). or on begin of next request. if (isShutDown() || SwingUtilities.isEventDispatchThread()) { // thread locals may not have been set when in AWT Thread IServiceProvider prev = testThreadLocals(); try { r.run(); } finally { unsetThreadLocals(prev); } } else { if (RequestCycle.get() != null) { requestEvents.get().add(r); } else synchronized (events) { events.add(r); } } } @Override protected void doInvokeLater(Runnable r, boolean immediate) { if (!immediate) invokeLater(r); else getScheduledExecutor().execute(r); } @Override public void shutDown(boolean force) { if (shuttingDown) return; shuttingDown = true; try { // first just execute all events that are waiting, but only when we are in request cycle if (RequestCycle.get() != null) executeEvents(); super.shutDown(force); if (executor != null) executor.destroy(); if (RequestCycle.get() != null && WebClientSession.get() != null) WebClientSession.get().logout(); //valueUnbound will do real shutdown else if (session != null) { try { session.invalidate(); } catch (Exception e) { } } } finally { shuttingDown = false; } } @SuppressWarnings("nls") @Override public void showDefaultLogin() throws ServoyException { // if no credentials are set then redirect to the solution loader page. if (credentials.getUserName() == null || credentials.getPassword() == null) { String solutionName = solutionRoot.getSolution() != null ? solutionRoot.getSolution().getName() : null; // close the solution, webclient can't handle a "half" open solution. solutionRoot.close(getActiveSolutionHandler()); Map<String, Object> map = new HashMap<String, Object>(); if (getPreferedSolutionNameToLoadOnInit() != null) { map.put("s", getPreferedSolutionNameToLoadOnInit()); if (getPreferedSolutionMethodNameToCall() != null) map.put("m", getPreferedSolutionMethodNameToCall()); if (getPreferedSolutionMethodArguments() != null && getPreferedSolutionMethodArguments().length > 0 && getPreferedSolutionMethodArguments()[0] != null) { map.put("a", getPreferedSolutionMethodArguments()[0]); } } else { map.put("s", solutionName); } getMainPage().setResponsePage(SolutionLoader.class, new PageParameters(map)); return; } super.showDefaultLogin(); } @Override public void logout(final Object[] solution_to_open_args) { if (getClientInfo().getUserUid() != null) { if (getSolution() != null) { // close solution first invokeLater(new Runnable() { public void run() { boolean doLogOut = getClientInfo().getUserUid() != null; if (getSolution() != null) { doLogOut = closeSolution(false, solution_to_open_args); } if (doLogOut && getSolution() == null) { credentials.clear(); getClientInfo().clearUserInfo(); } //remove cookies //TODO: make cookies remove through signIn form if (RequestCycle.get() != null) { WebRequest webRequest = ((WebRequestCycle)RequestCycle.get()).getWebRequest(); WebResponse webResponse = ((WebRequestCycle)RequestCycle.get()).getWebResponse(); Cookie password = webRequest.getCookie("signInForm.password"); if (password != null) { password.setMaxAge(0); password.setPath("/"); webResponse.addCookie(password); } } } }); } else { credentials.clear(); getClientInfo().clearUserInfo(); } } } //behaviour in webclient is different, we do shutdown the webclient instance ,since we cannot switch solution private boolean closing = false; public boolean isClosing() { return closing; } @SuppressWarnings("nls") @Override public boolean closeSolution(boolean force, Object[] args) { if (getSolution() == null || closing) return true; try { RequestCycle rc = RequestCycle.get(); closing = true; MainPage mp = MainPage.getRequestMainPage(); if (mp == null) { mp = getMainPage(); } // generate requests on all other reachable browser tabs/browser windows that are open in this client; // so that they can show the "page expired" page (even if AJAX timer is not enabled) List<String> triggerReqScripts = getTriggerReqOnOtherPagesJS(rc, mp); MainPage.ShowUrlInfo showUrlInfo = mp.getShowUrlInfo(); boolean shownInDialog = mp.isShowingInDialog() || mp.isClosingAsDivPopup(); // if this page is showing in a div dialog (or is about to be closed as it was in one), the page redirect needs to happen inside root page, not in iframe boolean retval = super.closeSolution(force, args); if (retval) { // reset path to templates such as servoy_webclient_default.css in case session/client are reused for another solution if (rc != null) putClientProperty(WEBCONSTANTS.WEBCLIENT_TEMPLATES_DIR, null); else { // for example when being closed from admin page invokeLater(new Runnable() { @Override public void run() { putClientProperty(WEBCONSTANTS.WEBCLIENT_TEMPLATES_DIR, null); } }); } if (rc != null && rc.getRequestTarget() instanceof AjaxRequestTarget) { // the idea of this line is to block all possible showurl calls generated by any RedirectAjaxRequestTargets arriving (after the solution is closed) on the page, // page that might want to actually show some (possibly external and slow) other page instead of page expired ((AjaxRequestTarget)rc.getRequestTarget()).appendJavascript("getRootServoyFrame().Servoy.redirectingOnSolutionClose = true;"); if (triggerReqScripts != null) { for (String js : triggerReqScripts) { ((AjaxRequestTarget)rc.getRequestTarget()).appendJavascript(js); } } } // close all windows getRuntimeWindowManager().closeFormInWindow(null, true); Collection<Style> userStyles = getFlattenedSolution().flushUserStyles(); if (userStyles != null) { for (Style style : userStyles) { ComponentFactory.flushStyle(this, style); } } getRuntimeProperties().put(IServiceProvider.RT_VALUELIST_CACHE, null); getRuntimeProperties().put(IServiceProvider.RT_OVERRIDESTYLE_CACHE, null); // what page should be shown next in browser? if (rc != null) { boolean showDefault = true; boolean urlShown = false; if (showUrlInfo != null) { showDefault = !"_self".equals(showUrlInfo.getTarget()) && !"_top".equals(showUrlInfo.getTarget()); String url = "/"; if (showUrlInfo.getUrl() != null) { url = showUrlInfo.getUrl(); } if (rc.getRequestTarget() instanceof AjaxRequestTarget) { showUrlInfo.setOnRootFrame(true); showUrlInfo.setUseIFrame(false); String show = MainPage.getShowUrlScript(showUrlInfo); if (show != null) { urlShown = true; ((AjaxRequestTarget)rc.getRequestTarget()).appendJavascript(show); // extra call to make sure that it is removed for the next time. mp.getShowUrlScript(); } } else { rc.setRequestTarget(new RedirectRequestTarget(url)); } } if (showDefault) { if (Session.exists() && RequestCycle.get() != null) { if (getPreferedSolutionNameToLoadOnInit() == null) { if ((urlShown || shownInDialog) && rc.getRequestTarget() instanceof AjaxRequestTarget) { // if this page is shown in a dialog then try to get the parent so that the page map is not included in the url MainPage page = mp; while ((page.isShowingInDialog() || page.isClosingAsDivPopup()) && page.getCallingContainer() != null) { page = page.getCallingContainer(); } CharSequence urlFor = page.urlFor(SelectSolution.class, null); ((AjaxRequestTarget)rc.getRequestTarget()).appendJavascript( MainPage.getShowUrlScript(new ShowUrlInfo(urlFor.toString(), "_self", null, 0, true, false))); } else { mp.setResponsePage(SelectSolution.class); } } else { // if solution browsing is false, make sure that the credentials are kept if (!Utils.getAsBoolean(Settings.getInstance().getProperty("servoy.allowSolutionBrowsing", "true"))) { WebClientSession.get().keepCredentials(getPreferedSolutionNameToLoadOnInit()); } Map<String, Object> map = new HashMap<String, Object>(); map.put("s", getPreferedSolutionNameToLoadOnInit()); map.put("m", getPreferedSolutionMethodNameToCall()); if (getPreferedSolutionMethodArguments() != null && getPreferedSolutionMethodArguments().length > 0) { map.put("a", getPreferedSolutionMethodArguments()[0]); } if ((urlShown || shownInDialog) && rc.getRequestTarget() instanceof AjaxRequestTarget) { CharSequence urlFor = mp.urlFor(SolutionLoader.class, new PageParameters(map)); ((AjaxRequestTarget)rc.getRequestTarget()).appendJavascript( MainPage.getShowUrlScript(new ShowUrlInfo(urlFor.toString(), "_self", null, 0, true, false))); } else { rc.setResponsePage(SolutionLoader.class, new PageParameters(map), null); } } } } } } return retval; } finally { closing = false; } } // generate JS requests on all other reachable browser tabs/browser windows that are open in this client private List<String> getTriggerReqOnOtherPagesJS(RequestCycle rc, MainPage currentPage) { List<String> triggerJSs = null; if (rc != null && rc.getRequestTarget() instanceof AjaxRequestTarget) { FormManager fm = (FormManager)getFormManager(); if (fm != null) { List<String> all = fm.getCreatedMainContainerKeys(); triggerJSs = new ArrayList<String>(all.size()); for (String key : all) { MainPage page = (MainPage)fm.getMainContainer(key); if (page != null && page != currentPage) // should always be != null { String tmp = page.getTriggerBrowserRequestJS(); if (tmp != null) triggerJSs.add(tmp); } } return triggerJSs; } } return triggerJSs; } @Override protected IFormManagerInternal createFormManager() { return new WebFormManager(this, getMainPage()); } @Override protected IClientPluginAccess createClientPluginAccess() { return new WebClientPluginAccessProvider(this); } public MainPage getMainPage() { if (getFormManager() == null) { // this happens the first time. Then the main container is created. return createMainPage(); } else { // after that the form manger has the current page return (MainPage)((FormManager)getFormManager()).getCurrentContainer(); } } private MainPage createMainPage() { return new MainPage(this); } @Override public void setTitle(String title) { if (!isShutDown()) getMainPage().setTitle(title); } @Override public boolean isShutDown() { return shuttingDown || super.isShutDown(); } @Override public void setStatusText(String text, String tooltip) { getMainPage().setStatusText(text); } @Override public boolean showURL(String url, String target, String target_options, int timeout, boolean onRootFrame) { MainPage mp = MainPage.getRequestMainPage(); if (mp == null) mp = getMainPage(); if (mp != null) { mp.setShowURLCMD(url, target, target_options, timeout, onRootFrame); return true; } return false; } @Override public void reportInfo(String message) { adminInfoQueue.add(message); } private final List<String> adminInfoQueue = Collections.synchronizedList(new ArrayList<String>()); public String getAdminInfo() { if (adminInfoQueue.size() > 0) return adminInfoQueue.remove(0); else return null; } @Override public Dimension getScreenSize() { int width = ((WebClientInfo)WebClientSession.get().getClientInfo()).getProperties().getScreenWidth(); int height = ((WebClientInfo)WebClientSession.get().getClientInfo()).getProperties().getScreenHeight(); int orientation = getMainPage().getOrientation(); if (orientation == -1) { orientation = ((ServoyWebClientInfo)WebClientSession.get().getClientInfo()).getOrientation(); } if (orientation == 90 || orientation == -90) { return new Dimension(height, width); } return new Dimension(width, height); } protected final Object onBeginRequestLock = new Object(); public void onBeginRequest(WebClientSession webClientSession) { Solution solution = getSolution(); if (solution != null) { synchronized (onBeginRequestLock) { long solutionLastModifiedTime = webClientSession.getSolutionLastModifiedTime(solution); if (solutionLastModifiedTime != -1 && solutionLastModifiedTime != solution.getLastModifiedTime()) { if (isClosing() || isShutDown()) { if (((WebRequest)RequestCycle.get().getRequest()).isAjax()) throw new AbortException(); else throw new RestartResponseException(Application.get().getHomePage()); } refreshI18NMessages(); ((IScriptSupport)getScriptEngine()).reload(); ((WebFormManager)getFormManager()).reload(); MainPage page = (MainPage)((WebFormManager)getFormManager()).getMainContainer(null); throw new RestartResponseException(page); } executeEvents(); } } } public void onEndRequest(@SuppressWarnings("unused") WebClientSession webClientSession) { userRequestProperties.clear(); // just to make sure that on the end of the request there are really no more events waiting. // if that is the case then copy them to the events for the next time (no much sense to do them now, everything is detached) List<Runnable> list = requestEvents.get(); if (list.size() > 0) { synchronized (events) { events.addAll(list); } } requestEvents.remove(); } private void writeObject(ObjectOutputStream stream) throws IOException { //serialize is not implemented } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { //serialize is not implemented } @SuppressWarnings("nls") public static boolean isMobile() { boolean isMobile = false; if (Session.exists()) { org.apache.wicket.request.ClientInfo info = Session.get().getClientInfo(); if (info instanceof WebClientInfo) { String userAgent = ((WebClientInfo)info).getProperties().getNavigatorUserAgent(); if (userAgent != null) { userAgent = userAgent.toLowerCase(); isMobile = userAgent.contains("android") || userAgent.contains("iphone") || userAgent.contains("ipad"); } } } return isMobile; } private IEventDispatcher<WicketEvent> executor; /** * */ @SuppressWarnings("nls") public final synchronized IEventDispatcher<WicketEvent> getEventDispatcher() { if (executor == null && Boolean.parseBoolean(Settings.getInstance().getProperty("servoy.webclient.startscriptthread", "false"))) { executor = createDispatcher(); Thread thread = new Thread(executor, "Executor,clientid:" + getClientID()); thread.setDaemon(true); thread.start(); } return executor; } /** * Method to create the {@link IEventDispatcher} runnable */ protected IEventDispatcher<WicketEvent> createDispatcher() { return new WicketEventDispatcher(this); } @Override protected void reinitializeDefaultProperties() { } }