]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java
Stop trying if UI loop is not supported.
[lgpl/argeo-commons.git] / org.argeo.cms.ui.rap / src / org / argeo / cms / web / AbstractCmsEntryPoint.java
1 package org.argeo.cms.web;
2
3 import static org.argeo.naming.SharedSecret.X_SHARED_SECRET;
4
5 import java.io.IOException;
6 import java.security.PrivilegedAction;
7 import java.util.HashMap;
8 import java.util.Map;
9
10 import javax.jcr.Node;
11 import javax.jcr.PathNotFoundException;
12 import javax.jcr.Property;
13 import javax.jcr.Repository;
14 import javax.jcr.RepositoryException;
15 import javax.jcr.Session;
16 import javax.jcr.nodetype.NodeType;
17 import javax.security.auth.Subject;
18 import javax.security.auth.callback.Callback;
19 import javax.security.auth.callback.UnsupportedCallbackException;
20 import javax.security.auth.login.LoginContext;
21 import javax.security.auth.login.LoginException;
22 import javax.servlet.http.HttpServletRequest;
23
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26 import org.argeo.api.NodeConstants;
27 import org.argeo.cms.CmsException;
28 import org.argeo.cms.auth.CurrentUser;
29 import org.argeo.cms.auth.HttpRequestCallback;
30 import org.argeo.cms.auth.HttpRequestCallbackHandler;
31 import org.argeo.cms.ui.CmsStyles;
32 import org.argeo.cms.ui.CmsView;
33 import org.argeo.eclipse.ui.specific.UiContext;
34 import org.argeo.jcr.JcrUtils;
35 import org.argeo.naming.AuthPassword;
36 import org.argeo.naming.SharedSecret;
37 import org.eclipse.rap.rwt.RWT;
38 import org.eclipse.rap.rwt.application.AbstractEntryPoint;
39 import org.eclipse.rap.rwt.client.WebClient;
40 import org.eclipse.rap.rwt.client.service.BrowserNavigation;
41 import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent;
42 import org.eclipse.rap.rwt.client.service.BrowserNavigationListener;
43 import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
44 import org.eclipse.swt.widgets.Composite;
45 import org.eclipse.swt.widgets.Display;
46 import org.eclipse.swt.widgets.Shell;
47
48 /** Manages history and navigation */
49 public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implements CmsView {
50 private static final long serialVersionUID = 906558779562569784L;
51
52 private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class);
53
54 // private final Subject subject;
55 private LoginContext loginContext;
56
57 private final Repository repository;
58 private final String workspace;
59 private final String defaultPath;
60 private final Map<String, String> factoryProperties;
61
62 // Current state
63 private Session session;
64 private Node node;
65 private String nodePath;// useful when changing auth
66 private String state;
67 private Throwable exception;
68
69 // Client services
70 private final JavaScriptExecutor jsExecutor;
71 private final BrowserNavigation browserNavigation;
72
73 public AbstractCmsEntryPoint(Repository repository, String workspace, String defaultPath,
74 Map<String, String> factoryProperties) {
75 this.repository = repository;
76 this.workspace = workspace;
77 this.defaultPath = defaultPath;
78 this.factoryProperties = new HashMap<String, String>(factoryProperties);
79 // subject = new Subject();
80
81 // Initial login
82 LoginContext lc;
83 try {
84 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
85 new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse()));
86 lc.login();
87 } catch (LoginException e) {
88 try {
89 lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
90 lc.login();
91 } catch (LoginException e1) {
92 throw new CmsException("Cannot log in as anonymous", e1);
93 }
94 }
95 authChange(lc);
96
97 jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
98 browserNavigation = RWT.getClient().getService(BrowserNavigation.class);
99 if (browserNavigation != null)
100 browserNavigation.addBrowserNavigationListener(new CmsNavigationListener());
101 }
102
103 @Override
104 protected Shell createShell(Display display) {
105 Shell shell = super.createShell(display);
106 shell.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_SHELL);
107 display.disposeExec(new Runnable() {
108
109 @Override
110 public void run() {
111 if (log.isTraceEnabled())
112 log.trace("Logging out " + session);
113 JcrUtils.logoutQuietly(session);
114 }
115 });
116 return shell;
117 }
118
119 @Override
120 protected final void createContents(final Composite parent) {
121 // UiContext.setData(CmsView.KEY, this);
122 CmsView.registerCmsView(parent.getShell(), this);
123 Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
124 @Override
125 public Void run() {
126 try {
127 initUi(parent);
128 } catch (Exception e) {
129 throw new CmsException("Cannot create entrypoint contents", e);
130 }
131 return null;
132 }
133 });
134 }
135
136 /** Create UI */
137 protected abstract void initUi(Composite parent);
138
139 /** Recreate UI after navigation or auth change */
140 protected abstract void refresh();
141
142 /**
143 * The node to return when no node was found (for authenticated users and
144 * anonymous)
145 */
146 // private Node getDefaultNode(Session session) throws RepositoryException {
147 // if (!session.hasPermission(defaultPath, "read")) {
148 // String userId = session.getUserID();
149 // if (userId.equals(NodeConstants.ROLE_ANONYMOUS))
150 // // TODO throw a special exception
151 // throw new CmsException("Login required");
152 // else
153 // throw new CmsException("Unauthorized");
154 // }
155 // return session.getNode(defaultPath);
156 // }
157
158 protected String getBaseTitle() {
159 return factoryProperties.get(WebClient.PAGE_TITLE);
160 }
161
162 public void navigateTo(String state) {
163 exception = null;
164 String title = setState(state);
165 doRefresh();
166 if (browserNavigation != null)
167 browserNavigation.pushState(state, title);
168 }
169
170 // @Override
171 // public synchronized Subject getSubject() {
172 // return subject;
173 // }
174
175 // @Override
176 // public LoginContext getLoginContext() {
177 // return loginContext;
178 // }
179 protected Subject getSubject() {
180 return loginContext.getSubject();
181 }
182
183 @Override
184 public boolean isAnonymous() {
185 return CurrentUser.isAnonymous(getSubject());
186 }
187
188 @Override
189 public synchronized void logout() {
190 if (loginContext == null)
191 throw new CmsException("Login context should not be null");
192 try {
193 CurrentUser.logoutCmsSession(loginContext.getSubject());
194 loginContext.logout();
195 LoginContext anonymousLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
196 anonymousLc.login();
197 authChange(anonymousLc);
198 } catch (LoginException e) {
199 log.error("Cannot logout", e);
200 }
201 }
202
203 @Override
204 public synchronized void authChange(LoginContext lc) {
205 if (lc == null)
206 throw new CmsException("Login context cannot be null");
207 // logout previous login context
208 if (this.loginContext != null)
209 try {
210 this.loginContext.logout();
211 } catch (LoginException e1) {
212 log.warn("Could not log out: " + e1);
213 }
214 this.loginContext = lc;
215 Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
216
217 @Override
218 public Void run() {
219 try {
220 JcrUtils.logoutQuietly(session);
221 session = repository.login(workspace);
222 if (nodePath != null)
223 try {
224 node = session.getNode(nodePath);
225 } catch (PathNotFoundException e) {
226 navigateTo("~");
227 }
228
229 // refresh UI
230 doRefresh();
231 } catch (RepositoryException e) {
232 throw new CmsException("Cannot perform auth change", e);
233 }
234 return null;
235 }
236
237 });
238 }
239
240 @Override
241 public void exception(final Throwable e) {
242 AbstractCmsEntryPoint.this.exception = e;
243 log.error("Unexpected exception in CMS", e);
244 doRefresh();
245 }
246
247 protected synchronized void doRefresh() {
248 Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
249 @Override
250 public Void run() {
251 refresh();
252 return null;
253 }
254 });
255 }
256
257 /** Sets the state of the entry point and retrieve the related JCR node. */
258 protected synchronized String setState(String newState) {
259 String previousState = this.state;
260
261 String newNodePath = null;
262 String prefix = null;
263 this.state = newState;
264 if (newState.equals("~"))
265 this.state = "";
266
267 try {
268 int firstSlash = state.indexOf('/');
269 if (firstSlash == 0) {
270 newNodePath = state;
271 prefix = "";
272 } else if (firstSlash > 0) {
273 prefix = state.substring(0, firstSlash);
274 newNodePath = state.substring(firstSlash);
275 } else {
276 newNodePath = defaultPath;
277 prefix = state;
278
279 }
280
281 // auth
282 int colonIndex = prefix.indexOf('$');
283 if (colonIndex > 0) {
284 SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix)) {
285
286 @Override
287 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
288 super.handle(callbacks);
289 // handle HTTP context
290 for (Callback callback : callbacks) {
291 if (callback instanceof HttpRequestCallback) {
292 ((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest());
293 ((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse());
294 }
295 }
296 }
297 };
298 LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
299 lc.login();
300 authChange(lc);// sets the node as well
301 // } else {
302 // // TODO check consistency
303 // }
304 } else {
305 Node newNode = null;
306 if (session.nodeExists(newNodePath))
307 newNode = session.getNode(newNodePath);
308 else {
309 // throw new CmsException("Data " + newNodePath + " does not exist");
310 newNode = null;
311 }
312 setNode(newNode);
313 }
314 String title = publishMetaData(getNode());
315
316 if (log.isTraceEnabled())
317 log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")");
318
319 return title;
320 } catch (Exception e) {
321 log.error("Cannot set state '" + state + "'", e);
322 if (state.equals("") || newState.equals("~") || newState.equals(previousState))
323 return "Unrecoverable exception : " + e.getClass().getSimpleName();
324 if (previousState.equals(""))
325 previousState = "~";
326 navigateTo(previousState);
327 throw new CmsException("Unexpected issue when accessing #" + newState, e);
328 }
329 }
330
331 private String publishMetaData(Node node) throws RepositoryException {
332 // Title
333 String title;
334 if (node != null && node.isNodeType(NodeType.MIX_TITLE) && node.hasProperty(Property.JCR_TITLE))
335 title = node.getProperty(Property.JCR_TITLE).getString() + " - " + getBaseTitle();
336 else
337 title = getBaseTitle();
338
339 HttpServletRequest request = UiContext.getHttpRequest();
340 if (request == null)
341 return null;
342
343 StringBuilder js = new StringBuilder();
344 if (title == null)
345 title = "";
346 title = title.replace("'", "\\'");// sanitize
347 js.append("document.title = '" + title + "';");
348 jsExecutor.execute(js.toString());
349 return title;
350 }
351
352 // Simply remove some illegal character
353 // private String clean(String stringToClean) {
354 // return stringToClean.replaceAll("'", "").replaceAll("\\n", "")
355 // .replaceAll("\\t", "");
356 // }
357
358 protected synchronized Node getNode() {
359 return node;
360 }
361
362 private synchronized void setNode(Node node) throws RepositoryException {
363 this.node = node;
364 this.nodePath = node == null ? null : node.getPath();
365 }
366
367 protected String getState() {
368 return state;
369 }
370
371 protected Throwable getException() {
372 return exception;
373 }
374
375 protected void resetException() {
376 exception = null;
377 }
378
379 protected Session getSession() {
380 return session;
381 }
382
383 private class CmsNavigationListener implements BrowserNavigationListener {
384 private static final long serialVersionUID = -3591018803430389270L;
385
386 @Override
387 public void navigated(BrowserNavigationEvent event) {
388 setState(event.getState());
389 doRefresh();
390 }
391 }
392 }