/*
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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.wicket.Page;
import org.apache.wicket.PageParameters;
import org.apache.wicket.Request;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.Session;
import org.apache.wicket.markup.html.DynamicWebResource;
import org.apache.wicket.markup.html.DynamicWebResource.ResourceState;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.util.crypt.ICrypt;
import com.servoy.j2db.IWebClientApplication;
import com.servoy.j2db.J2DBGlobals;
import com.servoy.j2db.persistence.IRepository;
import com.servoy.j2db.persistence.RootObjectMetaData;
import com.servoy.j2db.persistence.Solution;
import com.servoy.j2db.persistence.SolutionMetaData;
import com.servoy.j2db.scripting.StartupArguments;
import com.servoy.j2db.server.headlessclient.dnd.DNDSessionInfo;
import com.servoy.j2db.server.shared.ApplicationServerRegistry;
import com.servoy.j2db.server.shared.WebCredentials;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.Settings;
import com.servoy.j2db.util.Utils;
/**
* A session which holds the actual started client inside
*
* @author jblok
*/
public class WebClientSession extends WebSession
{
private static final long serialVersionUID = 1L;
private long solutionLastModifiedTime = -1;
private Solution previousSolution = null;
private HttpSession httpSession;
private final WebCredentials credentials = new WebCredentials();
private Object[] serveInfo;
private transient final DNDSessionInfo dndSessionInfo = new DNDSessionInfo();
private String keepCredentialsSolutionName;
private final boolean blockInput;
private final boolean hideLoadingIndicator;
private transient ICrypt crypt;
public static WebClientSession get()
{
if (exists())
{
return (WebClientSession)Session.get();
}
return null;
}
public WebClientSession(Request request)
{
super(request);
setTemplateDirectoryName("default"); //$NON-NLS-1$
blockInput = Boolean.valueOf(Settings.getInstance().getProperty("servoy.webclient.blockinputonrequest", "false")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$
hideLoadingIndicator = Boolean.valueOf(Settings.getInstance().getProperty("servoy.webclient.hideloadingindicator", "false")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$
}
@SuppressWarnings("nls")
public IWebClientApplication startSessionClient(RootObjectMetaData sd, String method, StartupArguments argumentsScope) throws Exception
{
String firstArgument = argumentsScope.getFirstArgument();
IWebClientApplication webClient = getWebClient();
if (webClient != null)
{
boolean solutionLoaded = webClient.getSolution() != null;
if (solutionLoaded && !webClient.closeSolution(false, null))
{
return webClient; // not allowed to close solution?
}
if (solutionLoaded && isSignedIn() && !Utils.getAsBoolean(Settings.getInstance().getProperty("servoy.allowSolutionBrowsing", "true")) &&
!sd.getName().equals(keepCredentialsSolutionName))
{
webClient.logout(null);
}
if (!isSignedIn())
{
SolutionMetaData smd = (SolutionMetaData)sd;
IRepository repository = ApplicationServerRegistry.get().getLocalRepository();
Solution sol = (Solution)repository.getActiveRootObject(smd.getName(), IRepository.SOLUTIONS);
if (sol.getLoginSolutionName() == null && sol.getLoginFormID() <= 0 && smd.getMustAuthenticate())
{
//signin first
throw new RestartResponseAtInterceptPageException(SignIn.class);
}
}
keepCredentialsSolutionName = null;
}
if (webClient == null || webClient.isShutDown())
{
HttpServletRequest req = ((WebRequest)RequestCycle.get().getRequest()).getHttpServletRequest();
httpSession = req.getSession();
webClient = createWebClient(req, credentials, method, firstArgument == null ? null : new Object[] { firstArgument, argumentsScope.toJSMap() },
sd.getName());
webClient.handleArguments(new String[] { sd.getName() }, argumentsScope);
if (RequestCycle.get() != null)
{
// if this is inside a request cycle set the service provider.
// will be reset by the detach of the RequestCycle.
J2DBGlobals.setServiceProvider(webClient);
}
setAttribute("servoy_webclient", webClient);
}
else
{
webClient.handleArguments(firstArgument != null ? new String[] { sd.getName(), method, firstArgument } : new String[] { sd.getName(), method },
argumentsScope);
}
webClient.handleClientUserUidChanged(null, ""); // fake first load
if (webClient.getSolution() != null) getSolutionLastModifiedTime(webClient.getSolution());
else
{
if (webClient.getPreferedSolutionNameToLoadOnInit() != null)
{
Map<String, Object> map = new HashMap<String, Object>();
map.put("s", webClient.getPreferedSolutionNameToLoadOnInit());
map.put("m", webClient.getPreferedSolutionMethodNameToCall());
if (webClient.getPreferedSolutionMethodArguments() != null && webClient.getPreferedSolutionMethodArguments().length > 0)
{
map.put("a", webClient.getPreferedSolutionMethodArguments()[0]);
}
throw new RestartResponseException(SolutionLoader.class, new PageParameters(map));
}
}
return webClient;
}
protected IWebClientApplication createWebClient(HttpServletRequest req, WebCredentials credentials, String method, Object[] methodArgs, String solution)
throws Exception
{
return new WebClient(req, credentials, method, methodArgs, solution);
}
public WebClient getWebClient()
{
return (WebClient)getAttribute("servoy_webclient"); //$NON-NLS-1$
}
public boolean authenticate(String u, String p)
{
if (ApplicationServerRegistry.get().checkDefaultServoyAuthorisation(u, p) != null)
{
credentials.setUserName(u);
credentials.setPassword(p);
return true;
}
return false;
}
/**
* Logout is being called by:
*
* 1> 2 javascript methods: js_logout and js_exit
*
* 2> 2 UserClient methods: closeSolution and shutDown
*
* 3> 1 ValueUnbound of the session a> Session time out b> Remove attribute when loading new Solution
*
* With 1 and 2 the session can be invalidated. With 3a the session is already invalidating.
*
* 3b the session shouldn't be invalidated.
*
* If logout calls invalidate then the value unbound will be called again that will call logout again.
*/
public void logout()
{
credentials.clear();
RequestCycle rc = RequestCycle.get();
if (rc != null)
{
rc.setRedirect(false);
invalidate();
}
else if (httpSession != null)
{
try
{
httpSession.invalidate();
}
catch (RuntimeException ex)
{
// ignore can be that it is already (being) invalidated.
}
}
httpSession = null;
}
public boolean isSignedIn()
{
return credentials.getUserName() != null && credentials.getPassword() != null;
}
public void setTemplateDirectoryName(String dirName)
{
String name = dirName;
if (name == null || name.toString().length() == 0)
{
name = "default$";// for $ meaning see ServoyResourceStreamLocator //$NON-NLS-1$
}
else
{
name = name + "$";// for $ meaning see ServoyResourceStreamLocator //$NON-NLS-1$
}
setStyle(name);
}
public String getTemplateDirectoryName()
{
String retval = getStyle();
return retval.substring(0, retval.length() - 1); // trim tailing $
}
/**
* @return
*/
public long getSolutionLastModifiedTime(Solution solution)
{
if (previousSolution == null || !solution.equals(previousSolution))
{
solutionLastModifiedTime = solution.getLastModifiedTime();
previousSolution = solution;
}
return solutionLastModifiedTime;
}
public void serveResource(String fname, byte[] bs, String mimetype, String contentDisposition)
{
serveInfo = new Object[] { fname, bs, mimetype, contentDisposition == null ? "attachment" : contentDisposition }; //$NON-NLS-1$
}
public boolean isServedResourceAttachment()
{
return serveInfo != null && serveInfo.length >= 4 && "attachment".equals(serveInfo[3]);
}
/**
* @see wicket.IResourceListener#onResourceRequested()
*/
public DynamicWebResource.ResourceState getResourceState()
{
DynamicWebResource.ResourceState resourceState = null;
if (serveInfo[0] != null && serveInfo[1] != null)
{
((WebResponse)RequestCycle.get().getResponse()).setHeader("Content-disposition", serveInfo[3] + "; filename=\"" + serveInfo[0] + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
resourceState = new DynamicWebResource.ResourceState()
{
private final byte[] data = (byte[])serveInfo[1];
private final String mime = (String)serveInfo[2];
@Override
public int getLength()
{
return data.length;
}
@Override
public byte[] getData()
{
return data;
}
@Override
public String getContentType()
{
return mime;
}
};
}
else
{
resourceState = new ResourceState()
{
@Override
public byte[] getData()
{
return new byte[0];
}
@Override
public String getContentType()
{
return null;
}
};
}
class ServeClearer implements Runnable
{
private Object[] toClear;
ServeClearer(Object[] ref)
{
toClear = ref;
}
public void run()
{
toClear[0] = null;//clear
toClear[1] = null;//clear
toClear[2] = null;//clear
toClear = null;//clear
}
}
// clear after one minute
getWebClient().getScheduledExecutor().schedule(new ServeClearer(serveInfo), 60, TimeUnit.SECONDS);
return resourceState;
}
public DNDSessionInfo getDNDSessionInfo()
{
return dndSessionInfo;
}
public boolean useAjax()
{
WebClient webClient = getWebClient();
return webClient != null && Utils.getAsBoolean(webClient.getRuntimeProperties().get("useAJAX"));
}
public void keepCredentials(String solutionName)
{
this.keepCredentialsSolutionName = solutionName;
}
@Override
public void invalidateNow()
{
if (Debug.tracing())
{
Debug.trace("Session invalidated for sessionid: " + getId());
}
super.invalidateNow();
}
/**
* @return
*/
public boolean blockRequest()
{
return blockInput;
}
public boolean hideLoadingIndicator()
{
return hideLoadingIndicator;
}
private final HashMap<Page, List<Page>> lockedPages = new HashMap<Page, List<Page>>();
private final ThreadLocal<List<Page>> toRelease = new ThreadLocal<List<Page>>()
{
@Override
protected java.util.List<Page> initialValue()
{
return new ArrayList<Page>();
};
};
public List<Page> getPagesToRelease()
{
return toRelease.get();
}
/**
* @param touchedPages
* @param mainPage
*/
public void wantsToLock(List<Page> touchedPages, MainPage mainPage)
{
List<Page> ownLockedPages = toRelease.get();
if (ownLockedPages.contains(mainPage)) return;
synchronized (lockedPages)
{
boolean found = touchedPages.size() > 0;
boolean released = false;
while (found)
{
for (Page touched : touchedPages)
{
// if the current touched pages are in the locked pages map and its not on our own locked pages list.
// something is waiting for that page to be released.
found = !ownLockedPages.contains(touched) && lockedPages.containsKey(touched);
if (found)
{
// release first all current pages so that the other thread can go on.
releaseLocks();
released = true;
try
{
// wait for the other to release the locked pages.
lockedPages.wait();
}
catch (InterruptedException e)
{
Debug.error(e);
}
break;
}
}
}
// if we did release our own pages, we have to lock them now again.
if (released)
{
for (Page touched : touchedPages)
{
getPage(touched.getPageMapName(), touched.getPath(), Page.LATEST_VERSION);
}
}
// add them to the threadlocal so that we know what to release for this thread in ondetach.
ownLockedPages.add(mainPage);
// mark the page as locked by the current touched pages.
lockedPages.put(mainPage, touchedPages);
}
}
@Override
protected void detach()
{
super.detach();
synchronized (lockedPages)
{
// remove all locks this thread has
for (Page page : toRelease.get())
{
lockedPages.remove(page);
}
toRelease.remove();
// notify other threads that are waiting because they released there own used page lock.
lockedPages.notifyAll();
}
}
/**
* @return
*/
public ICrypt getCrypt()
{
return crypt;
}
/**
* @param crypt the crypt to set
*/
public void setCrypt(ICrypt crypt)
{
this.crypt = crypt;
}
}