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}