001/**
002 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
003 *   This file is part of the LDP4j Project:
004 *     http://www.ldp4j.org/
005 *
006 *   Center for Open Middleware
007 *     http://www.centeropenmiddleware.com/
008 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
009 *   Copyright (C) 2014 Center for Open Middleware.
010 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
011 *   Licensed under the Apache License, Version 2.0 (the "License");
012 *   you may not use this file except in compliance with the License.
013 *   You may obtain a copy of the License at
014 *
015 *             http://www.apache.org/licenses/LICENSE-2.0
016 *
017 *   Unless required by applicable law or agreed to in writing, software
018 *   distributed under the License is distributed on an "AS IS" BASIS,
019 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 *   See the License for the specific language governing permissions and
021 *   limitations under the License.
022 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
023 *   Artifact    : org.ldp4j.framework:ldp4j-application-api:0.2.0
024 *   Bundle      : ldp4j-application-api-0.2.0.jar
025 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
026 */
027package org.ldp4j.application;
028
029import static com.google.common.base.Preconditions.checkNotNull;
030import static com.google.common.base.Preconditions.checkState;
031
032import java.lang.ref.ReferenceQueue;
033import java.lang.ref.WeakReference;
034import java.util.Map;
035import java.util.concurrent.atomic.AtomicBoolean;
036import java.util.concurrent.atomic.AtomicLong;
037
038import org.ldp4j.application.data.Individual;
039import org.ldp4j.application.data.Name;
040import org.ldp4j.application.ext.ResourceHandler;
041import org.ldp4j.application.session.ResourceSnapshot;
042import org.ldp4j.application.session.SessionTerminationException;
043import org.ldp4j.application.session.WriteSession;
044import org.ldp4j.application.session.WriteSessionException;
045import org.ldp4j.application.spi.RuntimeDelegate;
046import org.ldp4j.application.spi.ShutdownListener;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import com.google.common.base.MoreObjects;
051import com.google.common.collect.Maps;
052
053
054/**
055 * A proxy class that acts as a facade for interacting with the Application
056 * Engine proactively.
057 */
058public final class ApplicationContext {
059
060  private static final class ApplicationEngineSingleton {
061
062    private static final ApplicationContext SINGLETON=new ApplicationContext();
063
064    private ApplicationEngineSingleton() {
065    }
066
067  }
068
069  private final class State implements WriteSession {
070
071    private final long id;
072    private final WriteSession delegate;
073
074    private boolean disposed;
075    private boolean completed;
076
077    private State(WriteSession delegate) {
078      this.delegate = delegate;
079      this.id=SESSION_COUNTER.incrementAndGet();
080    }
081
082    private void doDispose() throws SessionTerminationException {
083      clearSession(this);
084      this.disposed=true;
085      this.delegate.close();
086    }
087
088    private void verifyExecutability() {
089      checkState(!this.disposed,"Session has already been disposed");
090      checkState(!this.completed,"Session has already been completed");
091    }
092
093    long id() {
094      return this.id;
095    }
096
097    synchronized void dispose() {
098      LOGGER.warn("Closing session {} which was not closed by the user...",this);
099      try {
100        doDispose();
101      } catch (SessionTerminationException e) {
102        LOGGER.error("Could not close session {}",this,e);
103      }
104    }
105
106    /**
107     * {@inheritDoc}
108     */
109    @Override
110    public synchronized <S extends ResourceSnapshot> S resolve(Class<? extends S> snapshotClass, Individual<?, ?> individual) {
111      verifyExecutability();
112      return this.delegate.resolve(snapshotClass,individual);
113    }
114
115    /**
116     * {@inheritDoc}
117     */
118    @Override
119    public synchronized <S extends ResourceSnapshot> S find(Class<? extends S> snapshotClass, Name<?> id, Class<? extends ResourceHandler> handlerClass) {
120      verifyExecutability();
121      return this.delegate.find(snapshotClass,id,handlerClass);
122    }
123
124    /**
125     * {@inheritDoc}
126     */
127    @Override
128    public synchronized void modify(ResourceSnapshot resource) {
129      verifyExecutability();
130      this.delegate.modify(resource);
131    }
132
133    /**
134     * {@inheritDoc}
135     */
136    @Override
137    public synchronized void delete(ResourceSnapshot resource) {
138      verifyExecutability();
139      this.delegate.delete(resource);
140    }
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    public synchronized void saveChanges() throws WriteSessionException {
147      verifyExecutability();
148      this.delegate.saveChanges();
149      this.completed=true;
150    }
151
152    /**
153     * {@inheritDoc}
154     */
155    @Override
156    public synchronized void discardChanges() throws WriteSessionException {
157      verifyExecutability();
158      this.delegate.discardChanges();
159      this.completed=true;
160    }
161
162    /**
163     * {@inheritDoc}
164     */
165    @Override
166    public synchronized void close() throws SessionTerminationException {
167      if(this.disposed) {
168        return;
169      }
170      doDispose();
171    }
172
173    /**
174     * {@inheritDoc}
175     */
176    @Override
177    public synchronized String toString() {
178      return
179        MoreObjects.
180          toStringHelper(getClass()).
181            add("id",this.id).
182            add("completed",this.completed).
183            add("disposed",this.disposed).
184            add("delegate",this.delegate).
185            toString();
186    }
187
188  }
189
190  private final class ContextWriteSession implements WriteSession {
191
192    private final State state;
193
194    private ContextWriteSession(State state) {
195      this.state = state;
196    }
197
198    /**
199     * {@inheritDoc}
200     */
201    @Override
202    public <S extends ResourceSnapshot> S resolve(Class<? extends S> snapshotClass, Individual<?, ?> individual) {
203      return this.state.resolve(snapshotClass,individual);
204    }
205
206    /**
207     * {@inheritDoc}
208     */
209    @Override
210    public <S extends ResourceSnapshot> S find(Class<? extends S> snapshotClass, Name<?> id, Class<? extends ResourceHandler> handlerClass) {
211      return this.state.find(snapshotClass,id,handlerClass);
212    }
213
214    /**
215     * {@inheritDoc}
216     */
217    @Override
218    public void modify(ResourceSnapshot resource) {
219      this.state.modify(resource);
220    }
221
222    /**
223     * {@inheritDoc}
224     */
225    @Override
226    public void delete(ResourceSnapshot resource) {
227      this.state.delete(resource);
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    public void saveChanges() throws WriteSessionException {
235      this.state.saveChanges();
236    }
237
238    /**
239     * {@inheritDoc}
240     */
241    @Override
242    public void discardChanges() throws WriteSessionException {
243      this.state.discardChanges();
244    }
245
246    /**
247     * {@inheritDoc}
248     */
249    @Override
250    public void close() throws SessionTerminationException {
251      this.state.close();
252    }
253
254    /**
255     * {@inheritDoc}
256     */
257    @Override
258    public String toString() {
259      return
260        MoreObjects.
261          toStringHelper(getClass()).
262            add("state",this.state).
263            toString();
264    }
265
266  }
267
268  private static final class ContextWriteSessionReference extends WeakReference<ContextWriteSession> {
269
270    private State state;
271
272    public ContextWriteSessionReference(ContextWriteSession referent, State state) {
273      super(referent,REFERENCE_QUEUE);
274      this.state = state;
275    }
276
277    State state() {
278      return this.state;
279    }
280
281    @Override
282    public String toString() {
283      return
284        MoreObjects.
285          toStringHelper(getClass()).
286            omitNullValues().
287              add("enqueued",super.isEnqueued()).
288              add("state",this.state).
289              toString();
290    }
291
292
293  }
294
295  private static class WriteSessionCleaner extends Thread {
296
297    private static WriteSessionCleaner instance;
298
299    private final AtomicBoolean terminate;
300
301    private WriteSessionCleaner() {
302      setPriority(Thread.MAX_PRIORITY);
303      setName("ApplicationContext-WriteSessionCleaner");
304      setDaemon(true);
305      setUncaughtExceptionHandler(
306        new UncaughtExceptionHandler() {
307          @Override
308          public void uncaughtException(Thread t, Throwable e) {
309            LOGGER.error("Cleaner thread unexpectedly died. Full stacktrace follows",e);
310            launch();
311          }
312        }
313      );
314      this.terminate=new AtomicBoolean(false);
315    }
316
317    @Override
318    public void run() {
319      while (!this.terminate.get()) {
320        try {
321          ContextWriteSessionReference ref=(ContextWriteSessionReference)REFERENCE_QUEUE.remove();
322          ApplicationContext.State session = ref.state();
323          LOGGER.trace("Session {} is now weakly reachable...",session);
324          session.dispose();
325        } catch (InterruptedException e) {
326          // ignore
327        }
328      }
329    }
330
331    static void launch() {
332      instance=new WriteSessionCleaner();
333      instance.start();
334    }
335
336    static void terminate() {
337      instance.terminate.set(true);
338      instance.interrupt();
339    }
340
341    static boolean isActive() {
342      return instance!=null;
343    }
344
345  }
346
347  private static final Logger LOGGER=LoggerFactory.getLogger(ApplicationContext.class);
348
349  private static final AtomicLong SESSION_COUNTER=new AtomicLong();
350
351  private static final ReferenceQueue<ContextWriteSession> REFERENCE_QUEUE=new ReferenceQueue<ContextWriteSession>();
352
353  private final RuntimeDelegate delegate;
354  private final Map<Long,ContextWriteSessionReference> references;
355  private final Map<Long,Long> sessionOwner;
356  private final Map<Long,Long> threadSession;
357
358  private ApplicationContext() {
359    this.delegate=RuntimeDelegate.getInstance();
360    this.references=Maps.newLinkedHashMap();
361    this.sessionOwner=Maps.newLinkedHashMap();
362    this.threadSession=Maps.newLinkedHashMap();
363    LOGGER.info("Initialized Application Context");
364  }
365
366  private ApplicationContextException failure(Throwable cause, String fmt, Object... args) {
367    String message=String.format(fmt,args);
368    if(cause!=null) {
369      LOGGER.error(message+". Full stacktrace follows",cause);
370    } else {
371      LOGGER.error(message);
372    }
373    return new ApplicationContextException(message,cause);
374  }
375
376  private synchronized boolean clearSession(State session) {
377    long sessionId=session.id();
378    long ownerId=this.sessionOwner.get(sessionId);
379    this.references.remove(sessionId);
380    this.sessionOwner.remove(sessionId);
381    this.threadSession.remove(ownerId);
382    return ownerId==Thread.currentThread().getId();
383  }
384
385  /**
386   * Create a {@code WriteSession}. Only one write session can be active per
387   * thread. Sessions should not be shared among threads.
388   *
389   * @return the write session
390   * @throws ApplicationContextException
391   *             if no write session can be created for whichever reason,
392   *             e.g., the Application Engine is off-line or is not available.
393   */
394  public synchronized WriteSession createSession() throws ApplicationContextException {
395    if(this.threadSession.containsKey(Thread.currentThread().getId())) {
396      throw failure(null,"Thread already has an active session");
397    }
398    if(this.delegate.isOffline()) {
399      throw failure(null,"The Application Engine is off-line");
400    }
401    if(!WriteSessionCleaner.isActive()) {
402      WriteSessionCleaner.launch();
403      this.delegate.registerShutdownListener(
404        new ShutdownListener(){
405          @Override
406          public void engineShutdown() {
407            WriteSessionCleaner.terminate();
408          }
409        }
410      );
411    }
412
413    WriteSession nativeSession=this.delegate.createSession();
414    if(nativeSession==null) {
415      throw failure(null,"Could not create native write session");
416    }
417    State state = new State(nativeSession);
418    ContextWriteSession leakedSession = new ContextWriteSession(state);
419    ContextWriteSessionReference reference = new ContextWriteSessionReference(leakedSession,state);
420    this.references.put(state.id(),reference);
421    this.sessionOwner.put(state.id(),Thread.currentThread().getId());
422    this.threadSession.put(Thread.currentThread().getId(),state.id());
423    return leakedSession;
424  }
425
426
427  /**
428   * Dispose a {@code WriteSession}. Once the session has been disposed it
429   * will not be active (usable) any longer.
430   *
431   * @param session
432   *            the session to be disposed
433   * @throws NullPointerException
434   *             if the session is null.
435   * @throws ApplicationContextException
436   *             if the session cannot be disposed, e.g., the session is not
437   *             owned by the current thread.
438   * @deprecated Use the {@link WriteSession#close()} method instead.
439   */
440  @Deprecated
441  public void disposeSession(WriteSession session) throws ApplicationContextException {
442    checkNotNull(session,"Session cannot be null");
443    if(!ContextWriteSession.class.isInstance(session)) {
444      throw failure(null,"Unknown session %s",session);
445    }
446    try {
447      session.close();
448    } catch (SessionTerminationException e) {
449      throw failure(e,"Could not close session '%X'",session.hashCode());
450    }
451  }
452
453  /**
454   * {@inheritDoc}
455   */
456  @Override
457  public synchronized String toString() {
458    return
459      MoreObjects.
460        toStringHelper(getClass()).
461          omitNullValues().
462          add("delegate",this.delegate).
463          add("references",this.references).
464          add("sessionOwner",this.sessionOwner).
465          add("threadSession",this.threadSession).
466          toString();
467  }
468
469  /**
470   * Get the Application context.
471   *
472   * @return the application context.
473   */
474  public static ApplicationContext getInstance() {
475    return ApplicationEngineSingleton.SINGLETON;
476  }
477
478}