]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeLogger.java
Make auth more robust
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / NodeLogger.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
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 package org.argeo.cms.internal.kernel;
17
18 import java.io.IOException;
19 import java.nio.file.FileSystems;
20 import java.nio.file.Path;
21 import java.nio.file.Paths;
22 import java.nio.file.StandardWatchEventKinds;
23 import java.nio.file.WatchEvent;
24 import java.nio.file.WatchKey;
25 import java.nio.file.WatchService;
26 import java.security.SignatureException;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.Enumeration;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.ListIterator;
35 import java.util.Map;
36 import java.util.Properties;
37 import java.util.concurrent.BlockingQueue;
38 import java.util.concurrent.LinkedBlockingQueue;
39
40 import org.apache.commons.logging.Log;
41 import org.apache.commons.logging.LogFactory;
42 import org.apache.log4j.AppenderSkeleton;
43 import org.apache.log4j.Level;
44 import org.apache.log4j.LogManager;
45 import org.apache.log4j.Logger;
46 import org.apache.log4j.PropertyConfigurator;
47 import org.apache.log4j.spi.LoggingEvent;
48 import org.argeo.cms.CmsException;
49 import org.argeo.cms.auth.CurrentUser;
50 import org.argeo.node.ArgeoLogListener;
51 import org.argeo.node.ArgeoLogger;
52 import org.argeo.node.NodeConstants;
53 import org.argeo.osgi.useradmin.UserAdminConf;
54 import org.osgi.framework.Constants;
55 import org.osgi.framework.ServiceReference;
56 import org.osgi.service.cm.ConfigurationAdmin;
57 import org.osgi.service.log.LogEntry;
58 import org.osgi.service.log.LogListener;
59 import org.osgi.service.log.LogReaderService;
60 import org.osgi.service.log.LogService;
61
62 /** Not meant to be used directly in standard log4j config */
63 class NodeLogger implements ArgeoLogger, LogListener {
64 /** Internal debug for development purposes. */
65 private static Boolean debug = false;
66
67 // private final static Log log = LogFactory.getLog(NodeLogger.class);
68
69 private Boolean disabled = false;
70
71 private String level = null;
72
73 private Level log4jLevel = null;
74 // private Layout layout;
75
76 private Properties configuration;
77
78 private AppenderImpl appender;
79
80 private final List<ArgeoLogListener> everythingListeners = Collections
81 .synchronizedList(new ArrayList<ArgeoLogListener>());
82 private final List<ArgeoLogListener> allUsersListeners = Collections
83 .synchronizedList(new ArrayList<ArgeoLogListener>());
84 private final Map<String, List<ArgeoLogListener>> userListeners = Collections
85 .synchronizedMap(new HashMap<String, List<ArgeoLogListener>>());
86
87 private BlockingQueue<LogEvent> events;
88 private LogDispatcherThread logDispatcherThread = new LogDispatcherThread();
89
90 private Integer maxLastEventsCount = 10 * 1000;
91
92 /** Marker to prevent stack overflow */
93 private ThreadLocal<Boolean> dispatching = new ThreadLocal<Boolean>() {
94
95 @Override
96 protected Boolean initialValue() {
97 return false;
98 }
99 };
100
101 @SuppressWarnings("unchecked")
102 public NodeLogger(LogReaderService lrs) {
103 Enumeration<LogEntry> logEntries = lrs.getLog();
104 while (logEntries.hasMoreElements())
105 logged(logEntries.nextElement());
106 lrs.addLogListener(this);
107
108 // configure log4j watcher
109 String log4jConfiguration = KernelUtils.getFrameworkProp("log4j.configuration");
110 if (log4jConfiguration != null && log4jConfiguration.startsWith("file:")) {
111 if (log4jConfiguration.contains("..")) {
112 if (log4jConfiguration.startsWith("file://"))
113 log4jConfiguration = log4jConfiguration.substring("file://".length());
114 else if (log4jConfiguration.startsWith("file:"))
115 log4jConfiguration = log4jConfiguration.substring("file:".length());
116 }
117 try {
118 Path log4jconfigPath = Paths.get(log4jConfiguration);
119 Thread log4jConfWatcher = new Log4jConfWatcherThread(log4jconfigPath);
120 log4jConfWatcher.start();
121 } catch (Exception e) {
122 stdErr("Badly formatted log4j configuration URI " + log4jConfiguration + ": " + e.getMessage());
123 }
124 }
125 }
126
127 public void init() {
128 try {
129 events = new LinkedBlockingQueue<LogEvent>();
130
131 // if (layout != null)
132 // setLayout(layout);
133 // else
134 // setLayout(new PatternLayout(pattern));
135 appender = new AppenderImpl();
136 reloadConfiguration();
137 Logger.getRootLogger().addAppender(appender);
138
139 logDispatcherThread = new LogDispatcherThread();
140 logDispatcherThread.start();
141 } catch (Exception e) {
142 throw new CmsException("Cannot initialize log4j");
143 }
144 }
145
146 public void destroy() throws Exception {
147 Logger.getRootLogger().removeAppender(appender);
148 allUsersListeners.clear();
149 for (List<ArgeoLogListener> lst : userListeners.values())
150 lst.clear();
151 userListeners.clear();
152
153 events.clear();
154 events = null;
155 logDispatcherThread.interrupt();
156 }
157
158 // public void setLayout(Layout layout) {
159 // this.layout = layout;
160 // }
161
162 public String toString() {
163 return "Node Logger";
164 }
165
166 //
167 // OSGi LOGGER
168 //
169 @Override
170 public void logged(LogEntry status) {
171 Log pluginLog = LogFactory.getLog(status.getBundle().getSymbolicName());
172 Integer severity = status.getLevel();
173 if (severity == LogService.LOG_ERROR) {
174 // FIXME Fix Argeo TP
175 if (status.getException() instanceof SignatureException)
176 return;
177 pluginLog.error(msg(status), status.getException());
178 } else if (severity == LogService.LOG_WARNING)
179 pluginLog.warn(msg(status), status.getException());
180 else if (severity == LogService.LOG_INFO && pluginLog.isDebugEnabled())
181 pluginLog.debug(msg(status), status.getException());
182 else if (severity == LogService.LOG_DEBUG && pluginLog.isTraceEnabled())
183 pluginLog.trace(msg(status), status.getException());
184 }
185
186 private String msg(LogEntry status) {
187 StringBuilder sb = new StringBuilder();
188 sb.append(status.getMessage());
189 ServiceReference<?> sr = status.getServiceReference();
190 if (sr != null) {
191 sb.append(' ');
192 String[] objectClasses = (String[]) sr.getProperty(Constants.OBJECTCLASS);
193 if (isSpringApplicationContext(objectClasses)) {
194 sb.append("{org.springframework.context.ApplicationContext}");
195 Object symbolicName = sr.getProperty(Constants.BUNDLE_SYMBOLICNAME);
196 if (symbolicName != null)
197 sb.append(" " + Constants.BUNDLE_SYMBOLICNAME + ": " + symbolicName);
198 } else {
199 sb.append(arrayToString(objectClasses));
200 }
201 Object cn = sr.getProperty(NodeConstants.CN);
202 if (cn != null)
203 sb.append(" " + NodeConstants.CN + ": " + cn);
204 Object factoryPid = sr.getProperty(ConfigurationAdmin.SERVICE_FACTORYPID);
205 if (factoryPid != null)
206 sb.append(" " + ConfigurationAdmin.SERVICE_FACTORYPID + ": " + factoryPid);
207 // else {
208 // Object servicePid = sr.getProperty(Constants.SERVICE_PID);
209 // if (servicePid != null)
210 // sb.append(" " + Constants.SERVICE_PID + ": " + servicePid);
211 // }
212 // servlets
213 Object whiteBoardPattern = sr.getProperty(KernelConstants.WHITEBOARD_PATTERN_PROP);
214 if (whiteBoardPattern != null) {
215 if (whiteBoardPattern instanceof String) {
216 sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": " + whiteBoardPattern);
217 } else {
218 sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": "
219 + arrayToString((String[]) whiteBoardPattern));
220 }
221 }
222 // RWT
223 Object contextName = sr.getProperty(KernelConstants.CONTEXT_NAME_PROP);
224 if (contextName != null)
225 sb.append(" " + KernelConstants.CONTEXT_NAME_PROP + ": " + contextName);
226
227 // user directories
228 Object baseDn = sr.getProperty(UserAdminConf.baseDn.name());
229 if (baseDn != null)
230 sb.append(" " + UserAdminConf.baseDn.name() + ": " + baseDn);
231
232 }
233 return sb.toString();
234 }
235
236 private String arrayToString(Object[] arr) {
237 StringBuilder sb = new StringBuilder();
238 sb.append('[');
239 for (int i = 0; i < arr.length; i++) {
240 if (i != 0)
241 sb.append(',');
242 sb.append(arr[i]);
243 }
244 sb.append(']');
245 return sb.toString();
246 }
247
248 private boolean isSpringApplicationContext(String[] objectClasses) {
249 for (String clss : objectClasses) {
250 if (clss.equals("org.eclipse.gemini.blueprint.context.DelegatedExecutionOsgiBundleApplicationContext")) {
251 return true;
252 }
253 }
254 return false;
255 }
256
257 //
258 // ARGEO LOGGER
259 //
260
261 public synchronized void register(ArgeoLogListener listener, Integer numberOfPreviousEvents) {
262 String username = CurrentUser.getUsername();
263 if (username == null)
264 throw new CmsException("Only authenticated users can register a log listener");
265
266 if (!userListeners.containsKey(username)) {
267 List<ArgeoLogListener> lst = Collections.synchronizedList(new ArrayList<ArgeoLogListener>());
268 userListeners.put(username, lst);
269 }
270 userListeners.get(username).add(listener);
271 List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(username, numberOfPreviousEvents);
272 for (LogEvent evt : lastEvents)
273 dispatchEvent(listener, evt);
274 }
275
276 public synchronized void registerForAll(ArgeoLogListener listener, Integer numberOfPreviousEvents,
277 boolean everything) {
278 if (everything)
279 everythingListeners.add(listener);
280 else
281 allUsersListeners.add(listener);
282 List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(null, numberOfPreviousEvents);
283 for (LogEvent evt : lastEvents)
284 if (everything || evt.getUsername() != null)
285 dispatchEvent(listener, evt);
286 }
287
288 public synchronized void unregister(ArgeoLogListener listener) {
289 String username = CurrentUser.getUsername();
290 if (username == null)// FIXME
291 return;
292 if (!userListeners.containsKey(username))
293 throw new CmsException("No user listeners " + listener + " registered for user " + username);
294 if (!userListeners.get(username).contains(listener))
295 throw new CmsException("No user listeners " + listener + " registered for user " + username);
296 userListeners.get(username).remove(listener);
297 if (userListeners.get(username).isEmpty())
298 userListeners.remove(username);
299
300 }
301
302 public synchronized void unregisterForAll(ArgeoLogListener listener) {
303 everythingListeners.remove(listener);
304 allUsersListeners.remove(listener);
305 }
306
307 /** For development purpose, since using regular logging is not easy here */
308 private static void stdOut(Object obj) {
309 System.out.println(obj);
310 }
311
312 private static void stdErr(Object obj) {
313 System.err.println(obj);
314 }
315
316 private static void debug(Object obj) {
317 if (debug)
318 System.out.println(obj);
319 }
320
321 private static boolean isInternalDebugEnabled() {
322 return debug;
323 }
324
325 // public void setPattern(String pattern) {
326 // this.pattern = pattern;
327 // }
328
329 public void setDisabled(Boolean disabled) {
330 this.disabled = disabled;
331 }
332
333 public void setLevel(String level) {
334 this.level = level;
335 }
336
337 public void setConfiguration(Properties configuration) {
338 this.configuration = configuration;
339 }
340
341 public void updateConfiguration(Properties configuration) {
342 setConfiguration(configuration);
343 reloadConfiguration();
344 }
345
346 public Properties getConfiguration() {
347 return configuration;
348 }
349
350 /**
351 * Reloads configuration (if the configuration {@link Properties} is set)
352 */
353 protected void reloadConfiguration() {
354 if (configuration != null) {
355 LogManager.resetConfiguration();
356 PropertyConfigurator.configure(configuration);
357 }
358 }
359
360 protected synchronized void processLoggingEvent(LogEvent event) {
361 if (disabled)
362 return;
363
364 if (dispatching.get())
365 return;
366
367 if (level != null && !level.trim().equals("")) {
368 if (log4jLevel == null || !log4jLevel.toString().equals(level))
369 try {
370 log4jLevel = Level.toLevel(level);
371 } catch (Exception e) {
372 System.err.println("Log4j level could not be set for level '" + level + "', resetting it to null.");
373 e.printStackTrace();
374 level = null;
375 }
376
377 if (log4jLevel != null && !event.getLoggingEvent().getLevel().isGreaterOrEqual(log4jLevel)) {
378 return;
379 }
380 }
381
382 try {
383 // admin listeners
384 Iterator<ArgeoLogListener> everythingIt = everythingListeners.iterator();
385 while (everythingIt.hasNext())
386 dispatchEvent(everythingIt.next(), event);
387
388 if (event.getUsername() != null) {
389 Iterator<ArgeoLogListener> allUsersIt = allUsersListeners.iterator();
390 while (allUsersIt.hasNext())
391 dispatchEvent(allUsersIt.next(), event);
392
393 if (userListeners.containsKey(event.getUsername())) {
394 Iterator<ArgeoLogListener> userIt = userListeners.get(event.getUsername()).iterator();
395 while (userIt.hasNext())
396 dispatchEvent(userIt.next(), event);
397 }
398 }
399 } catch (Exception e) {
400 stdOut("Cannot process logging event");
401 e.printStackTrace();
402 }
403 }
404
405 protected void dispatchEvent(ArgeoLogListener logListener, LogEvent evt) {
406 LoggingEvent event = evt.getLoggingEvent();
407 logListener.appendLog(evt.getUsername(), event.getTimeStamp(), event.getLevel().toString(),
408 event.getLoggerName(), event.getThreadName(), event.getMessage(), event.getThrowableStrRep());
409 }
410
411 private class AppenderImpl extends AppenderSkeleton {
412 public boolean requiresLayout() {
413 return false;
414 }
415
416 public void close() {
417 }
418
419 @Override
420 protected void append(LoggingEvent event) {
421 if (events != null) {
422 try {
423 String username = CurrentUser.getUsername();
424 events.put(new LogEvent(username, event));
425 } catch (InterruptedException e) {
426 // silent
427 }
428 }
429 }
430
431 }
432
433 private class LogDispatcherThread extends Thread {
434 /** encapsulated in order to simplify concurrency management */
435 private LinkedList<LogEvent> lastEvents = new LinkedList<LogEvent>();
436
437 public LogDispatcherThread() {
438 super("Argeo Logging Dispatcher Thread");
439 }
440
441 public void run() {
442 while (events != null) {
443 try {
444 LogEvent loggingEvent = events.take();
445 processLoggingEvent(loggingEvent);
446 addLastEvent(loggingEvent);
447 } catch (InterruptedException e) {
448 if (events == null)
449 return;
450 }
451 }
452 }
453
454 protected synchronized void addLastEvent(LogEvent loggingEvent) {
455 if (lastEvents.size() >= maxLastEventsCount)
456 lastEvents.poll();
457 lastEvents.add(loggingEvent);
458 }
459
460 public synchronized List<LogEvent> getLastEvents(String username, Integer maxCount) {
461 LinkedList<LogEvent> evts = new LinkedList<LogEvent>();
462 ListIterator<LogEvent> it = lastEvents.listIterator(lastEvents.size());
463 int count = 0;
464 while (it.hasPrevious() && (count < maxCount)) {
465 LogEvent evt = it.previous();
466 if (username == null || username.equals(evt.getUsername())) {
467 evts.push(evt);
468 count++;
469 }
470 }
471 return evts;
472 }
473 }
474
475 private class LogEvent {
476 private final String username;
477 private final LoggingEvent loggingEvent;
478
479 public LogEvent(String username, LoggingEvent loggingEvent) {
480 super();
481 this.username = username;
482 this.loggingEvent = loggingEvent;
483 }
484
485 @Override
486 public int hashCode() {
487 return loggingEvent.hashCode();
488 }
489
490 @Override
491 public boolean equals(Object obj) {
492 return loggingEvent.equals(obj);
493 }
494
495 @Override
496 public String toString() {
497 return username + "@ " + loggingEvent.toString();
498 }
499
500 public String getUsername() {
501 return username;
502 }
503
504 public LoggingEvent getLoggingEvent() {
505 return loggingEvent;
506 }
507
508 }
509
510 private class Log4jConfWatcherThread extends Thread {
511 private Path log4jConfigurationPath;
512
513 public Log4jConfWatcherThread(Path log4jConfigurationPath) {
514 super("Log4j Configuration Watcher");
515 try {
516 this.log4jConfigurationPath = log4jConfigurationPath.toRealPath();
517 } catch (IOException e) {
518 this.log4jConfigurationPath = log4jConfigurationPath.toAbsolutePath();
519 stdOut("Cannot determine real path for " + log4jConfigurationPath + ": " + e.getMessage());
520 }
521 }
522
523 public void run() {
524 Path parentDir = log4jConfigurationPath.getParent();
525 try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
526 parentDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
527 WatchKey wk;
528 watching: while ((wk = watchService.take()) != null) {
529 for (WatchEvent<?> event : wk.pollEvents()) {
530 final Path changed = (Path) event.context();
531 if (log4jConfigurationPath.equals(parentDir.resolve(changed))) {
532 if (isInternalDebugEnabled())
533 debug(log4jConfigurationPath + " has changed, reloading.");
534 PropertyConfigurator.configure(log4jConfigurationPath.toUri().toURL());
535 }
536 }
537 // reset the key
538 boolean valid = wk.reset();
539 if (!valid) {
540 break watching;
541 }
542 }
543 } catch (IOException | InterruptedException e) {
544 stdErr("Log4j configuration watcher failed: " + e.getMessage());
545 }
546 }
547 }
548 }