]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ThreadBoundJcrSessionFactory.java
Keep improving security
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jcr / src / main / java / org / argeo / jcr / ThreadBoundJcrSessionFactory.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.argeo.jcr;
18
19 import java.lang.reflect.InvocationHandler;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Proxy;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Map;
28
29 import javax.jcr.LoginException;
30 import javax.jcr.Repository;
31 import javax.jcr.RepositoryException;
32 import javax.jcr.Session;
33 import javax.jcr.SimpleCredentials;
34
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.argeo.ArgeoException;
38 import org.springframework.beans.factory.DisposableBean;
39 import org.springframework.beans.factory.FactoryBean;
40 import org.springframework.beans.factory.InitializingBean;
41
42 /** Proxy JCR sessions and attach them to calling threads. */
43 public class ThreadBoundJcrSessionFactory implements FactoryBean,
44 InitializingBean, DisposableBean {
45 private final static Log log = LogFactory
46 .getLog(ThreadBoundJcrSessionFactory.class);
47
48 private Repository repository;
49
50 private ThreadLocal<Session> session = new ThreadLocal<Session>();
51 private final Session proxiedSession;
52 /** If workspace is null, default will be used. */
53 private String workspace = null;
54
55 private String defaultUsername = "demo";
56 private String defaultPassword = "demo";
57 private Boolean forceDefaultCredentials = false;
58
59 private boolean active = true;
60
61 // monitoring
62 private final List<Thread> threads = Collections
63 .synchronizedList(new ArrayList<Thread>());
64 private final Map<Long, Session> activeSessions = Collections
65 .synchronizedMap(new HashMap<Long, Session>());
66 private MonitoringThread monitoringThread;
67
68 public ThreadBoundJcrSessionFactory() {
69 Class<?>[] interfaces = { Session.class };
70 proxiedSession = (Session) Proxy.newProxyInstance(getClass()
71 .getClassLoader(), interfaces,
72 new JcrSessionInvocationHandler());
73 }
74
75 /** Logs in to the repository using various strategies. */
76 protected Session login() {
77 if (!isActive())
78 throw new ArgeoException("Thread bound session factory inactive");
79
80 // discard session previously attached to this thread
81 Thread thread = Thread.currentThread();
82 if (activeSessions.containsKey(thread.getId())) {
83 Session oldSession = activeSessions.remove(thread.getId());
84 oldSession.logout();
85 session.remove();
86 }
87
88 Session newSession = null;
89 // first try to login without credentials, assuming the underlying login
90 // module will have dealt with authentication (typically using Spring
91 // Security)
92 if (!forceDefaultCredentials)
93 try {
94 newSession = repository.login(workspace);
95 } catch (LoginException e1) {
96 log.warn("Cannot login without credentials: " + e1.getMessage());
97 // invalid credentials, go to the next step
98 } catch (RepositoryException e1) {
99 // other kind of exception, fail
100 throw new ArgeoException("Cannot log in to repository", e1);
101 }
102
103 // log using default username / password (useful for testing purposes)
104 if (newSession == null)
105 try {
106 SimpleCredentials sc = new SimpleCredentials(defaultUsername,
107 defaultPassword.toCharArray());
108 newSession = repository.login(sc, workspace);
109 } catch (RepositoryException e) {
110 throw new ArgeoException("Cannot log in to repository", e);
111 }
112
113 session.set(newSession);
114 // Log and monitor new session
115 if (log.isTraceEnabled())
116 log.trace("Logged in to JCR session " + newSession + "; userId="
117 + newSession.getUserID());
118
119 // monitoring
120 activeSessions.put(thread.getId(), newSession);
121 threads.add(thread);
122 return newSession;
123 }
124
125 public Object getObject() {
126 return proxiedSession;
127 }
128
129 public void afterPropertiesSet() throws Exception {
130 monitoringThread = new MonitoringThread();
131 monitoringThread.start();
132 }
133
134 public synchronized void destroy() throws Exception {
135 if (activeSessions.size() == 0)
136 return;
137
138 if (log.isDebugEnabled())
139 log.debug("Cleaning up " + activeSessions.size()
140 + " active JCR sessions...");
141
142 deactivate();
143 for (Session sess : activeSessions.values()) {
144 sess.logout();
145 }
146 activeSessions.clear();
147 }
148
149 protected Boolean isActive() {
150 return active;
151 }
152
153 protected synchronized void deactivate() {
154 active = false;
155 notifyAll();
156 }
157
158 protected synchronized void removeSession(Thread thread) {
159 if (!isActive())
160 return;
161 activeSessions.remove(thread.getId());
162 threads.remove(thread);
163 }
164
165 protected synchronized void cleanDeadThreads() {
166 if (!isActive())
167 return;
168 Iterator<Thread> it = threads.iterator();
169 while (it.hasNext()) {
170 Thread thread = it.next();
171 if (!thread.isAlive() && isActive()) {
172 if (activeSessions.containsKey(thread.getId())) {
173 Session session = activeSessions.get(thread.getId());
174 activeSessions.remove(thread.getId());
175 session.logout();
176 if (log.isDebugEnabled())
177 log.debug("Cleaned up JCR session (userID="
178 + session.getUserID() + ") from dead thread "
179 + thread.getId());
180 }
181 it.remove();
182 }
183 }
184 try {
185 wait(1000);
186 } catch (InterruptedException e) {
187 // silent
188 }
189 }
190
191 public Class<? extends Session> getObjectType() {
192 return Session.class;
193 }
194
195 public boolean isSingleton() {
196 return true;
197 }
198
199 /**
200 * Called before a method is actually called, allowing to check the session
201 * or re-login it (e.g. if authentication has changed). The default
202 * implementation returns the session.
203 */
204 protected Session preCall(Session session) {
205 return session;
206 }
207
208 public void setRepository(Repository repository) {
209 this.repository = repository;
210 }
211
212 public void setDefaultUsername(String defaultUsername) {
213 this.defaultUsername = defaultUsername;
214 }
215
216 public void setDefaultPassword(String defaultPassword) {
217 this.defaultPassword = defaultPassword;
218 }
219
220 public void setForceDefaultCredentials(Boolean forceDefaultCredentials) {
221 this.forceDefaultCredentials = forceDefaultCredentials;
222 }
223
224 public void setWorkspace(String workspace) {
225 this.workspace = workspace;
226 }
227
228 protected class JcrSessionInvocationHandler implements InvocationHandler {
229
230 public Object invoke(Object proxy, Method method, Object[] args)
231 throws Throwable {
232 Session threadSession = session.get();
233 if (threadSession == null) {
234 if ("logout".equals(method.getName()))// no need to login
235 return Void.TYPE;
236 else if ("toString".equals(method.getName()))// maybe logging
237 return "Uninitialized Argeo thread bound JCR session";
238 threadSession = login();
239 }
240
241 Object ret = method.invoke(threadSession, args);
242 if ("logout".equals(method.getName())) {
243 session.remove();
244 Thread thread = Thread.currentThread();
245 removeSession(thread);
246 if (log.isTraceEnabled())
247 log.trace("Logged out JCR session (userId="
248 + threadSession.getUserID() + ") on thread "
249 + thread.getId());
250 }
251 return ret;
252 }
253 }
254
255 /** Monitors registered thread in order to clean up dead ones. */
256 private class MonitoringThread extends Thread {
257
258 @Override
259 public void run() {
260 while (isActive()) {
261 cleanDeadThreads();
262 }
263 }
264
265 }
266 }