View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.common;
20  
21  import static org.mycore.common.events.MCRSessionEvent.Type.activated;
22  import static org.mycore.common.events.MCRSessionEvent.Type.passivated;
23  
24  import java.net.InetAddress;
25  import java.net.URI;
26  import java.net.UnknownHostException;
27  import java.util.Collections;
28  import java.util.Hashtable;
29  import java.util.Iterator;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Map.Entry;
35  import java.util.Objects;
36  import java.util.Optional;
37  import java.util.Queue;
38  import java.util.Set;
39  import java.util.UUID;
40  import java.util.concurrent.CompletableFuture;
41  import java.util.concurrent.ExecutorService;
42  import java.util.concurrent.Executors;
43  import java.util.concurrent.ThreadFactory;
44  import java.util.concurrent.TimeUnit;
45  import java.util.concurrent.atomic.AtomicInteger;
46  import java.util.function.Supplier;
47  
48  import org.apache.logging.log4j.LogManager;
49  import org.apache.logging.log4j.Logger;
50  import org.mycore.common.config.MCRConfiguration2;
51  import org.mycore.common.events.MCRSessionEvent;
52  import org.mycore.common.events.MCRShutdownHandler;
53  import org.mycore.common.events.MCRShutdownHandler.Closeable;
54  import org.mycore.util.concurrent.MCRTransactionableRunnable;
55  
56  import com.google.common.util.concurrent.ThreadFactoryBuilder;
57  
58  /**
59   * Instances of this class collect information kept during a session like the currently active user, the preferred
60   * language etc.
61   *
62   * @author Detlev Degenhardt
63   * @author Jens Kupferschmidt
64   * @author Frank Lützenkirchen
65   * @version $Revision$ $Date$
66   */
67  public class MCRSession implements Cloneable {
68  
69      private static final URI DEFAULT_URI = URI.create("");
70  
71      /** A map storing arbitrary session data * */
72      private Map<Object, Object> map = new Hashtable<>();
73  
74      @SuppressWarnings("unchecked")
75      private Map.Entry<Object, Object>[] emptyEntryArray = new Map.Entry[0];
76  
77      private List<Map.Entry<Object, Object>> mapEntries;
78  
79      private boolean mapChanged = true;
80  
81      AtomicInteger accessCount;
82  
83      AtomicInteger concurrentAccess;
84  
85      ThreadLocal<AtomicInteger> currentThreadCount = ThreadLocal.withInitial(AtomicInteger::new);
86  
87      /** the logger */
88      static Logger LOGGER = LogManager.getLogger(MCRSession.class.getName());
89  
90      /** The user ID of the session */
91      private MCRUserInformation userInformation;
92  
93      /** The language for this session as upper case character */
94      private String language = null;
95  
96      private Locale locale = null;
97  
98      /** The unique ID of this session */
99      private String sessionID;
100 
101     private String ip;
102 
103     private long loginTime, lastAccessTime, thisAccessTime, createTime;
104 
105     private StackTraceElement[] constructingStackTrace;
106 
107     private Optional<URI> firstURI = Optional.empty();
108 
109     private ThreadLocal<Throwable> lastActivatedStackTrace = new ThreadLocal<>();
110 
111     private ThreadLocal<Queue<Runnable>> onCommitTasks = ThreadLocal.withInitial(LinkedList::new);
112 
113     private static ExecutorService COMMIT_SERVICE;
114 
115     private static MCRUserInformation guestUserInformation = MCRSystemUserInformation.getGuestInstance();
116 
117     static {
118         ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("MCRSession-OnCommitService-#%d")
119             .build();
120         COMMIT_SERVICE = Executors.newFixedThreadPool(4, threadFactory);
121         MCRShutdownHandler.getInstance().addCloseable(new Closeable() {
122 
123             @Override
124             public void prepareClose() {
125                 COMMIT_SERVICE.shutdown();
126             }
127 
128             @Override
129             public int getPriority() {
130                 return Integer.MIN_VALUE + 8;
131             }
132 
133             @Override
134             public void close() {
135                 if (!COMMIT_SERVICE.isTerminated()) {
136                     try {
137                         COMMIT_SERVICE.awaitTermination(10, TimeUnit.MINUTES);
138                     } catch (InterruptedException e) {
139                         LOGGER.warn("Error while waiting for shutdown.", e);
140                     }
141                 }
142             }
143 
144         });
145     }
146 
147     /**
148      * The constructor of a MCRSession. As default the user ID is set to the value of the property variable named
149      * 'MCR.Users.Guestuser.UserName'.
150      */
151     MCRSession() {
152         userInformation = guestUserInformation;
153         setCurrentLanguage(MCRConfiguration2.getString("MCR.Metadata.DefaultLang").orElse(MCRConstants.DEFAULT_LANG));
154         accessCount = new AtomicInteger();
155         concurrentAccess = new AtomicInteger();
156 
157         ip = "";
158         sessionID = buildSessionID();
159         MCRSessionMgr.addSession(this);
160 
161         LOGGER.debug("MCRSession created {}", sessionID);
162         setLoginTime();
163         createTime = loginTime;
164         Throwable t = new Throwable();
165         t.fillInStackTrace();
166         constructingStackTrace = t.getStackTrace();
167     }
168 
169     protected final void setLoginTime() {
170         loginTime = System.currentTimeMillis();
171         lastAccessTime = loginTime;
172         thisAccessTime = loginTime;
173     }
174 
175     /**
176      * Constructs a unique session ID for this session, based on current time and IP address of host where the code
177      * runs.
178      */
179     private static String buildSessionID() {
180         return UUID.randomUUID().toString();
181     }
182 
183     /**
184      * Returns the unique ID of this session
185      */
186     public String getID() {
187         return sessionID;
188     }
189 
190     /**
191      * Returns a list of all stored object keys within MCRSession. This method is not thread safe. I you need thread
192      * safe access to all stored objects use {@link MCRSession#getMapEntries()} instead.
193      *
194      * @return Returns a list of all stored object keys within MCRSession as java.util.Ierator
195      */
196     public Iterator<Object> getObjectsKeyList() {
197         return Collections.unmodifiableSet(map.keySet()).iterator();
198     }
199 
200     /**
201      * Returns an unmodifiable list of all entries in this MCRSession This method is thread safe.
202      */
203     public List<Map.Entry<Object, Object>> getMapEntries() {
204         if (mapChanged) {
205             mapChanged = false;
206             final Set<Entry<Object, Object>> entrySet = Collections.unmodifiableMap(map).entrySet();
207             final Map.Entry<Object, Object>[] entryArray = entrySet.toArray(emptyEntryArray);
208             mapEntries = List.of(entryArray);
209         }
210         return mapEntries;
211     }
212 
213     /** returns the current language */
214     public final String getCurrentLanguage() {
215         return language;
216     }
217 
218     /** sets the current language */
219     public final void setCurrentLanguage(String language) {
220         Locale newLocale = Locale.forLanguageTag(language);
221         this.language = language;
222         this.locale = newLocale;
223     }
224 
225     public Locale getLocale() {
226         return locale;
227     }
228 
229     /** Write data to the logger for debugging purposes */
230     public final void debug() {
231         LOGGER.debug("SessionID = {}", sessionID);
232         LOGGER.debug("UserID    = {}", getUserInformation().getUserID());
233         LOGGER.debug("IP        = {}", ip);
234         LOGGER.debug("language  = {}", language);
235     }
236 
237     /** Stores an object under the given key within the session * */
238     public Object put(Object key, Object value) {
239         mapChanged = true;
240         return map.put(key, value);
241     }
242 
243     /** Returns the object that was stored in the session under the given key * */
244     public Object get(Object key) {
245         return map.get(key);
246     }
247 
248     public void deleteObject(Object key) {
249         mapChanged = true;
250         map.remove(key);
251     }
252 
253     /** Get the current ip value */
254     public String getCurrentIP() {
255         return ip;
256     }
257 
258     /** Set the ip to the given IP */
259     public final void setCurrentIP(String newip) {
260         //a necessary condition for an IP address is to start with an hexadecimal value or ':'
261         if (Character.digit(newip.charAt(0), 16) == -1 && newip.charAt(0) != ':') {
262             LOGGER.error("Is not a valid IP address: {}", newip);
263             return;
264         }
265         try {
266             InetAddress inetAddress = InetAddress.getByName(newip);
267             ip = inetAddress.getHostAddress();
268         } catch (UnknownHostException e) {
269             LOGGER.error("Exception while parsing new ip {} using old value.", newip, e);
270         }
271     }
272 
273     public final long getLoginTime() {
274         return loginTime;
275     }
276 
277     public void close() {
278         // remove from session list
279         LOGGER.debug("Remove myself from MCRSession list");
280         MCRSessionMgr.removeSession(this);
281         // clear bound objects
282         LOGGER.debug("Clearing local map.");
283         map.clear();
284         mapEntries = null;
285         sessionID = null;
286     }
287 
288     @Override
289     public String toString() {
290         return "MCRSession[" + getID() + ",user:'" + getUserInformation().getUserID() + "',ip:" + getCurrentIP()
291             + "]";
292     }
293 
294     public long getLastAccessedTime() {
295         return lastAccessTime;
296     }
297 
298     public void setFirstURI(Supplier<URI> uri) {
299         if (firstURI.isEmpty()) {
300             firstURI = Optional.of(uri.get());
301         }
302     }
303 
304     /**
305      * Activate this session. For internal use mainly by MCRSessionMgr.
306      *
307      * @see MCRSessionMgr#setCurrentSession(MCRSession)
308      */
309     void activate() {
310         lastAccessTime = thisAccessTime;
311         thisAccessTime = System.currentTimeMillis();
312         accessCount.incrementAndGet();
313         if (currentThreadCount.get().getAndIncrement() == 0) {
314             lastActivatedStackTrace.set(new RuntimeException("This is for debugging purposes only"));
315             fireSessionEvent(activated, concurrentAccess.incrementAndGet());
316         } else {
317             MCRException e = new MCRException(
318                 "Cannot activate a Session more than once per thread: " + currentThreadCount.get().get());
319             LOGGER.warn("Too many activate() calls stacktrace:", e);
320             LOGGER.warn("First activate() call stacktrace:", lastActivatedStackTrace.get());
321         }
322     }
323 
324     /**
325      * Passivate this session. For internal use mainly by MCRSessionMgr.
326      *
327      * @see MCRSessionMgr#releaseCurrentSession()
328      */
329     void passivate() {
330         if (currentThreadCount.get().getAndDecrement() == 1) {
331             lastActivatedStackTrace.set(null);
332             fireSessionEvent(passivated, concurrentAccess.decrementAndGet());
333         } else {
334             LOGGER.debug("deactivate currentThreadCount: {}", currentThreadCount.get().get());
335         }
336         if (firstURI.isEmpty()) {
337             firstURI = Optional.of(DEFAULT_URI);
338         }
339         onCommitTasks.remove();
340     }
341 
342     /**
343      * Fire MCRSessionEvents. This is a common method that fires all types of MCRSessionEvent. Mainly for internal use
344      * of MCRSession and MCRSessionMgr.
345      *
346      * @param type
347      *            type of event
348      * @param concurrentAccessors
349      *            number of concurrentThreads (passivateEvent gets 0 for singleThread)
350      */
351     void fireSessionEvent(MCRSessionEvent.Type type, int concurrentAccessors) {
352         MCRSessionEvent event = new MCRSessionEvent(this, type, concurrentAccessors);
353         LOGGER.debug(event);
354         MCRSessionMgr.getListeners().forEach(l -> l.sessionEvent(event));
355     }
356 
357     public long getThisAccessTime() {
358         return thisAccessTime;
359     }
360 
361     public long getCreateTime() {
362         return createTime;
363     }
364 
365     /**
366      * starts a new database transaction.
367      */
368     @Deprecated
369     public void beginTransaction() {
370         MCRTransactionHelper.beginTransaction();
371     }
372 
373     /**
374      * Determine whether the current resource transaction has been marked for rollback.
375      * @return boolean indicating whether the transaction has been marked for rollback
376      */
377     @Deprecated
378     public boolean transactionRequiresRollback() {
379         return MCRTransactionHelper.transactionRequiresRollback();
380     }
381 
382     /**
383      * commits the database transaction. Commit is only done if {@link #isTransactionActive()} returns true.
384      */
385     @Deprecated
386     public void commitTransaction() {
387         MCRTransactionHelper.commitTransaction();
388     }
389 
390     /**
391      * forces the database transaction to roll back. Roll back is only performed if {@link #isTransactionActive()}
392      * returns true.
393      */
394     @Deprecated
395     public void rollbackTransaction() {
396         MCRTransactionHelper.rollbackTransaction();
397     }
398 
399     /**
400      * Is the transaction still alive?
401      *
402      * @return true if the transaction is still alive
403      */
404     @Deprecated
405     public boolean isTransactionActive() {
406         return MCRTransactionHelper.isTransactionActive();
407     }
408 
409     public StackTraceElement[] getConstructingStackTrace() {
410         return constructingStackTrace;
411     }
412 
413     public Optional<URI> getFirstURI() {
414         return firstURI;
415     }
416 
417     /**
418      * @return the userInformation
419      */
420     public MCRUserInformation getUserInformation() {
421         return userInformation;
422     }
423 
424     /**
425      * @param userSystemAdapter
426      *            the userInformation to set
427      * @throws IllegalArgumentException if transition to new user information is forbidden (privilege escalation)
428      */
429     public void setUserInformation(MCRUserInformation userSystemAdapter) {
430         //check for MCR-1400
431         if (!isTransitionAllowed(userSystemAdapter)) {
432             throw new IllegalArgumentException("User transition from "
433                 + getUserInformation().getUserID()
434                 + " to " + userSystemAdapter.getUserID()
435                 + " is not permitted within the same session.");
436         }
437         this.userInformation = userSystemAdapter;
438         setLoginTime();
439     }
440 
441     /**
442      * Add a task which will be executed after {@link #commitTransaction()} was called.
443      *
444      * @param task thread witch will be executed after an commit
445      */
446     public void onCommit(Runnable task) {
447         this.onCommitTasks.get().offer(Objects.requireNonNull(task));
448     }
449 
450     protected void submitOnCommitTasks() {
451         Queue<Runnable> runnables = onCommitTasks.get();
452         onCommitTasks.remove();
453         CompletableFuture.allOf(runnables.stream()
454             .map(r -> new MCRTransactionableRunnable(r, this))
455             .map(MCRSession::toCompletableFuture)
456             .toArray(CompletableFuture[]::new))
457             .join();
458     }
459 
460     private static CompletableFuture<?> toCompletableFuture(MCRTransactionableRunnable r) {
461         try {
462             return CompletableFuture.runAsync(r, COMMIT_SERVICE);
463         } catch (RuntimeException e) {
464             LOGGER.error("Could not submit onCommit task. Running it locally.", e);
465             try {
466                 r.run();
467             } catch (RuntimeException e2) {
468                 LOGGER.fatal("Argh! Could not run task either. This task is lost 😰", e2);
469             }
470             return CompletableFuture.completedFuture(null);
471         }
472     }
473 
474     private boolean isTransitionAllowed(MCRUserInformation userSystemAdapter) {
475         //allow if current user super user or system user or not logged in
476         if (MCRSystemUserInformation.getSuperUserInstance().getUserID().equals(userInformation.getUserID())
477             || MCRSystemUserInformation.getGuestInstance().getUserID().equals(userInformation.getUserID())
478             || MCRSystemUserInformation.getSystemUserInstance().getUserID().equals(userInformation.getUserID())) {
479             return true;
480         }
481         //allow if new user information has default rights of guest user
482         //or userID equals old userID
483         return MCRSystemUserInformation.getGuestInstance().getUserID().equals(userSystemAdapter.getUserID())
484             || MCRSystemUserInformation.getSystemUserInstance().getUserID().equals(userSystemAdapter.getUserID())
485             || userInformation.getUserID().equals(userSystemAdapter.getUserID());
486     }
487 
488 }