/** tests timer behavior in a WebPage. */ @Test public void addToWebPage() { Duration dur = Duration.seconds(20); final MyAjaxSelfUpdatingTimerBehavior timer = new MyAjaxSelfUpdatingTimerBehavior(dur); final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent(); Label label = new Label(MockPageWithLinkAndComponent.COMPONENT_ID, "Hello"); page.add(label); page.add( new Link<Void>(MockPageWithLinkAndComponent.LINK_ID) { private static final long serialVersionUID = 1L; @Override public void onClick() { // do nothing, link is just used to simulate a roundtrip } }); label.setOutputMarkupId(true); label.add(timer); tester.startPage(page); validate(timer, true); tester.clickLink(MockPageWithLinkAndComponent.LINK_ID); validate(timer, true); }
/** Tests timer behavior in a component added to an AjaxRequestTarget */ @Test public void addToAjaxUpdate() { Duration dur = Duration.seconds(20); final MyAjaxSelfUpdatingTimerBehavior timer = new MyAjaxSelfUpdatingTimerBehavior(dur); final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent(); page.add(new WebComponent(MockPageWithLinkAndComponent.COMPONENT_ID).setOutputMarkupId(true)); page.add( new AjaxLink<Void>(MockPageWithLinkAndComponent.LINK_ID) { private static final long serialVersionUID = 1L; @Override public void onClick(AjaxRequestTarget target) { WebMarkupContainer wmc = new WebMarkupContainer(MockPageWithLinkAndComponent.COMPONENT_ID); wmc.setOutputMarkupId(true); wmc.add(timer); page.replace(wmc); target.add(wmc); } }); tester.startPage(page); tester.clickLink(MockPageWithLinkAndComponent.LINK_ID); validate(timer, false); }
/** @throws Exception */ @Test public void testSerialization() throws Exception { // a simple worker that acquires a lock on page 5 class Locker extends Thread { private final PageAccessSynchronizer sync; public Locker(PageAccessSynchronizer sync) { this.sync = sync; } @Override public void run() { sync.lockPage(5); } } // set up a synchronizer and lock page 5 with locker1 final Duration timeout = Duration.seconds(30); final PageAccessSynchronizer sync = new PageAccessSynchronizer(timeout); Locker locker1 = new Locker(sync); final long start = System.currentTimeMillis(); locker1.run(); // make sure we can serialize the synchronizer final PageAccessSynchronizer sync2 = WicketObjects.cloneObject(sync); assertTrue(sync != sync2); // make sure the clone does not retain locks by attempting to lock page locked by locker1 in // locker2 Locker locker2 = new Locker(sync2); locker2.run(); assertTrue(Duration.milliseconds(System.currentTimeMillis() - start).lessThan(timeout)); }
/** * Verifies that {@link StoredResponsesMap} will expire the oldest entry if it is older than 2 * seconds * * @throws Exception */ @Test public void entriesLife2Seconds() throws Exception { StoredResponsesMap map = new StoredResponsesMap(1000, Duration.seconds(2)); assertEquals(0, map.size()); map.put("1", new BufferedWebResponse(null)); assertEquals(1, map.size()); TimeUnit.SECONDS.sleep(3); map.put("2", new BufferedWebResponse(null)); assertEquals(1, map.size()); assertTrue(map.containsKey("2")); }
/** @throws Exception */ @Test public void testBlocking() throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5)); final Duration hold = Duration.seconds(1); final Time t1locks[] = new Time[1]; final Time t2locks[] = new Time[1]; class T1 extends Thread { @Override public void run() { sync.lockPage(1); t1locks[0] = Time.now(); hold.sleep(); sync.unlockAllPages(); } } class T2 extends Thread { @Override public void run() { sync.lockPage(1); t2locks[0] = Time.now(); sync.unlockAllPages(); } } T1 t1 = new T1(); t1.setName("t1"); T2 t2 = new T2(); t2.setName("t2"); t1.start(); Duration.milliseconds(100).sleep(); t2.start(); t1.join(); t2.join(); assertTrue(!t2locks[0].before(t1locks[0].add(hold))); }
@Override protected void onInitialize() { ContentViewOrEditPanel contentViewOrEditPanel = new ContentViewOrEditPanel("contentViewOrEditPanel", (IModel<Product>) getDefaultModel()); SubCategoryViewOrEditPanel subCategoryViewOrEditPanel = new SubCategoryViewOrEditPanel( "subCategoryViewOrEditPanel", (IModel<Product>) getDefaultModel()); Form<Product> productEditForm = new Form<Product>("productEditForm"); productEditForm.setModel( new CompoundPropertyModel<Product>((IModel<Product>) getDefaultModel())); productEditForm.add(new TextField<String>("number")); productEditForm.add(new TextField<String>("name")); productEditForm.add(new TextArea<String>("description")); productEditForm.add( new UrlTextField(ITEM_URL, new PropertyModel<String>(getDefaultModelObject(), ITEM_URL))); productEditForm.add(new NumberTextField<Integer>("amount")); productEditForm.add(new NumberTextField<Integer>("discount")); productEditForm.add(new NumberTextField<Integer>("shippingCost")); productEditForm.add(new NumberTextField<Integer>("tax")); productEditForm.add(new NumberTextField<Integer>("itemHeight")); productEditForm.add(new TextField<String>("itemHeightUnit")); productEditForm.add(new NumberTextField<Integer>("itemLength")); productEditForm.add(new TextField<String>("itemLengthUnit")); productEditForm.add(new NumberTextField<Integer>("itemWeight")); productEditForm.add(new TextField<String>("itemWeightUnit")); productEditForm.add(new NumberTextField<Integer>("itemWidth")); productEditForm.add(new TextField<String>("itemWidthUnit")); productEditForm.add(new BootstrapCheckbox("bestsellers")); productEditForm.add(new BootstrapCheckbox("latestCollection")); productEditForm.add(new NumberTextField<Integer>("rating")); productEditForm.add(new BootstrapCheckbox("recommended")); productEditForm.add(new NumberTextField<Integer>("stock.maxQuantity")); productEditForm.add(new NumberTextField<Integer>("stock.minQuantity")); productEditForm.add(new NumberTextField<Integer>("stock.quantity")); add(productEditForm.setOutputMarkupId(true)); add( contentViewOrEditPanel .add(contentViewOrEditPanel.new ContentEditFragement()) .setOutputMarkupId(true)); add( subCategoryViewOrEditPanel .add(subCategoryViewOrEditPanel.new SubCategoryEditFragement()) .setOutputMarkupId(true)); add(new NotificationPanel("feedback").hideAfter(Duration.seconds(5)).setOutputMarkupId(true)); add(new CancelAjaxLink().setOutputMarkupId(true)); add(new SaveAjaxButton(productEditForm).setOutputMarkupId(true)); super.onInitialize(); }
/** https://issues.apache.org/jira/browse/WICKET-4009 */ @Test public void unlockIfNoSuchPage() { PageAccessSynchronizer synchronizer = new PageAccessSynchronizer(Duration.seconds(2)); IPageManager pageManager = new MockPageManager(); IPageManager synchronizedPageManager = synchronizer.adapt(pageManager); synchronizedPageManager.getPage(0); ConcurrentMap<Integer, PageLock> locks = synchronizer.getLocks().get(); PageLock pageLock = locks.get(Integer.valueOf(0)); assertNull(pageLock); int pageId = 1; IManageablePage page = new MockPage(pageId); synchronizedPageManager.touchPage(page); synchronizedPageManager.getPage(pageId); PageLock pageLock2 = locks.get(Integer.valueOf(pageId)); assertNotNull(pageLock2); }
/** * https://issues.apache.org/jira/browse/WICKET-5316 * * @throws Exception */ @Test public void failToReleaseUnderLoad() throws Exception { final Duration duration = Duration.seconds(20); /* seconds */ final ConcurrentLinkedQueue<Exception> errors = new ConcurrentLinkedQueue<Exception>(); final long endTime = System.currentTimeMillis() + duration.getMilliseconds(); // set the synchronizer timeout one second longer than the test runs to prevent // starvation to become an issue final PageAccessSynchronizer sync = new PageAccessSynchronizer(duration.add(Duration.ONE_SECOND)); final CountDownLatch latch = new CountDownLatch(100); for (int count = 0; count < 100; count++) { new Thread() { @Override public void run() { try { while (System.currentTimeMillis() < endTime) { try { logger.debug(Thread.currentThread().getName() + " locking"); sync.lockPage(0); Thread.sleep(1); logger.debug(Thread.currentThread().getName() + " locked"); sync.unlockAllPages(); logger.debug(Thread.currentThread().getName() + " unlocked"); Thread.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } } } catch (Exception e) { logger.error(e.getMessage(), e); errors.add(e); } finally { latch.countDown(); } } }.start(); } latch.await(); if (!errors.isEmpty()) { logger.error("Number of lock errors that occurred: {}", errors.size()); throw errors.remove(); } }
/** * <a href="https://issues.apache.org/jira/browse/WICKET-3769">WICKET-3769</a> * * @throws Exception */ @Test public void applicationAndSessionAreExported() throws Exception { // bind the session so it can be found in TestSessionFilter tester.getSession().bind(); // execute TestSessionFilter in different thread so that the Application and the Session are // not set by WicketTester Thread testThread = new Thread( new Runnable() { public void run() { try { TestSessionFilter sessionFilter = new TestSessionFilter(tester); Assert.assertFalse(Application.exists()); Assert.assertFalse(Session.exists()); sessionFilter.doFilter( tester.getRequest(), tester.getResponse(), new TestFilterChain()); Assert.assertFalse(Application.exists()); Assert.assertFalse(Session.exists()); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } }); final StringBuilder failMessage = new StringBuilder(); final AtomicBoolean passed = new AtomicBoolean(true); testThread.setUncaughtExceptionHandler( new UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { failMessage.append(e.getMessage()); passed.set(false); } }); testThread.start(); testThread.join(Duration.seconds(1).getMilliseconds()); Assert.assertTrue(failMessage.toString(), passed.get()); }
/** * <a href="https://issues.apache.org/jira/browse/WICKET-3736">WICKET-3736</a> * * <p>Tries to simulate heavy load on the {@link StoredResponsesMap} by putting many entries and * removing randomly them. * * <p>The test is disabled by default because it is slow (~ 30secs). Enable it when we have * categorized tests ({@link Category}) and run slow ones only at Apache CI servers * * @throws InterruptedException */ @Test public void heavyLoad() throws InterruptedException { final int numberOfThreads = 100; final int iterations = 1000; final CountDownLatch startLatch = new CountDownLatch(numberOfThreads); final CountDownLatch endLatch = new CountDownLatch(numberOfThreads); final SecureRandom rnd = new SecureRandom(); final StoredResponsesMap map = new StoredResponsesMap(1000, Duration.seconds(60)); final List<String> keys = new CopyOnWriteArrayList<String>(); final Runnable r = new Runnable() { @Override public void run() { startLatch.countDown(); try { // wait all threads before starting the test startLatch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } for (int i = 0; i < iterations; i++) { String key = "abc" + (rnd.nextDouble() * iterations); keys.add(key); map.put(key, new BufferedWebResponse(null)); int randomMax = keys.size() - 1; int toRemove = randomMax == 0 ? 0 : rnd.nextInt(randomMax); String key2 = keys.get(toRemove); map.remove(key2); } endLatch.countDown(); } }; for (int t = 0; t < numberOfThreads; t++) { new Thread(r).start(); } endLatch.await(); }
public UpdateStateBehavior(String event) { super(event); setThrottleDelay(Duration.seconds(0.4)); }
public AsyncUpdatePanel(String id, IModel<V> callableParameterModel) { this(id, callableParameterModel, Duration.seconds(DEFAULT_TIMER_DURATION)); }
/** * @param pages * @param workers * @param duration * @throws Exception */ public void runContentionTest(final int pages, final int workers, final Duration duration) throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(1)); final AtomicInteger[] counts = new AtomicInteger[pages]; for (int i = 0; i < counts.length; i++) { counts[i] = new AtomicInteger(); } final AtomicInteger hits = new AtomicInteger(); final String[] error = new String[1]; class Worker extends Thread { @Override public void run() { Random random = new Random(); Time start = Time.now(); while (start.elapsedSince().lessThan(duration) && error[0] == null) { logger.info( "{} elapsed: {}, duration: {}", new Object[] {Thread.currentThread().getName(), start.elapsedSince(), duration}); int page1 = random.nextInt(counts.length); int page2 = random.nextInt(counts.length); int count = 0; while (page2 == page1 && count < 100) { page2 = random.nextInt(counts.length); count++; } if (page2 == page1) { throw new RuntimeException("orly?"); } try { sync.lockPage(page1); sync.lockPage(page2); // have locks, increment the count counts[page1].incrementAndGet(); counts[page2].incrementAndGet(); hits.incrementAndGet(); // hold the lock for some time try { Thread.sleep(50); } catch (InterruptedException e) { error[0] = "Worker :" + Thread.currentThread().getName() + " interrupted"; } // decrement the counts counts[page1].decrementAndGet(); counts[page2].decrementAndGet(); // release lock } catch (CouldNotLockPageException e) { // ignore } finally { sync.unlockAllPages(); } } } } class Monitor extends Thread { volatile boolean stop = false; @Override public void run() { while (!stop && error[0] == null) { for (int i = 0; i < counts.length; i++) { int count = counts[i].get(); if (count < 0 || count > 1) { error[0] = "Detected count of: " + count + " for page: " + i; return; } } try { Thread.sleep(1); } catch (InterruptedException e) { error[0] = "Monitor thread interrupted"; } } } } Monitor monitor = new Monitor(); monitor.setName("monitor"); monitor.start(); Worker[] bots = new Worker[workers]; for (int i = 0; i < bots.length; i++) { bots[i] = new Worker(); bots[i].setName("worker " + i); bots[i].start(); } for (Worker bot : bots) { bot.join(); } monitor.stop = true; monitor.join(); assertNull(error[0], error[0]); assertTrue(hits.get() >= counts.length); }
/** @throws Exception */ @Test public void testReentrant() throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5)); sync.lockPage(0); sync.lockPage(0); }
@Category(SlowTests.class) public class PageAccessSynchronizerTest extends Assert { private static final Logger logger = LoggerFactory.getLogger(PageAccessSynchronizerTest.class); /** */ @Rule public Timeout globalTimeout = new Timeout((int) Duration.seconds(30).getMilliseconds()); /** @throws Exception */ @Test public void testReentrant() throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5)); sync.lockPage(0); sync.lockPage(0); } /** @throws Exception */ @Test public void testBlocking() throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(5)); final Duration hold = Duration.seconds(1); final Time t1locks[] = new Time[1]; final Time t2locks[] = new Time[1]; class T1 extends Thread { @Override public void run() { sync.lockPage(1); t1locks[0] = Time.now(); hold.sleep(); sync.unlockAllPages(); } } class T2 extends Thread { @Override public void run() { sync.lockPage(1); t2locks[0] = Time.now(); sync.unlockAllPages(); } } T1 t1 = new T1(); t1.setName("t1"); T2 t2 = new T2(); t2.setName("t2"); t1.start(); Duration.milliseconds(100).sleep(); t2.start(); t1.join(); t2.join(); assertTrue(!t2locks[0].before(t1locks[0].add(hold))); } /** * @param pages * @param workers * @param duration * @throws Exception */ public void runContentionTest(final int pages, final int workers, final Duration duration) throws Exception { final PageAccessSynchronizer sync = new PageAccessSynchronizer(Duration.seconds(1)); final AtomicInteger[] counts = new AtomicInteger[pages]; for (int i = 0; i < counts.length; i++) { counts[i] = new AtomicInteger(); } final AtomicInteger hits = new AtomicInteger(); final String[] error = new String[1]; class Worker extends Thread { @Override public void run() { Random random = new Random(); Time start = Time.now(); while (start.elapsedSince().lessThan(duration) && error[0] == null) { logger.info( "{} elapsed: {}, duration: {}", new Object[] {Thread.currentThread().getName(), start.elapsedSince(), duration}); int page1 = random.nextInt(counts.length); int page2 = random.nextInt(counts.length); int count = 0; while (page2 == page1 && count < 100) { page2 = random.nextInt(counts.length); count++; } if (page2 == page1) { throw new RuntimeException("orly?"); } try { sync.lockPage(page1); sync.lockPage(page2); // have locks, increment the count counts[page1].incrementAndGet(); counts[page2].incrementAndGet(); hits.incrementAndGet(); // hold the lock for some time try { Thread.sleep(50); } catch (InterruptedException e) { error[0] = "Worker :" + Thread.currentThread().getName() + " interrupted"; } // decrement the counts counts[page1].decrementAndGet(); counts[page2].decrementAndGet(); // release lock } catch (CouldNotLockPageException e) { // ignore } finally { sync.unlockAllPages(); } } } } class Monitor extends Thread { volatile boolean stop = false; @Override public void run() { while (!stop && error[0] == null) { for (int i = 0; i < counts.length; i++) { int count = counts[i].get(); if (count < 0 || count > 1) { error[0] = "Detected count of: " + count + " for page: " + i; return; } } try { Thread.sleep(1); } catch (InterruptedException e) { error[0] = "Monitor thread interrupted"; } } } } Monitor monitor = new Monitor(); monitor.setName("monitor"); monitor.start(); Worker[] bots = new Worker[workers]; for (int i = 0; i < bots.length; i++) { bots[i] = new Worker(); bots[i].setName("worker " + i); bots[i].start(); } for (Worker bot : bots) { bot.join(); } monitor.stop = true; monitor.join(); assertNull(error[0], error[0]); assertTrue(hits.get() >= counts.length); } /** @throws Exception */ @Test public void testConcurrency() throws Exception { runContentionTest(20, 10, Duration.seconds(10)); } /** @throws Exception */ @Test public void testContention() throws Exception { runContentionTest(10, 20, Duration.seconds(10)); } /** @throws Exception */ @Test public void testSerialization() throws Exception { // a simple worker that acquires a lock on page 5 class Locker extends Thread { private final PageAccessSynchronizer sync; public Locker(PageAccessSynchronizer sync) { this.sync = sync; } @Override public void run() { sync.lockPage(5); } } // set up a synchronizer and lock page 5 with locker1 final Duration timeout = Duration.seconds(30); final PageAccessSynchronizer sync = new PageAccessSynchronizer(timeout); Locker locker1 = new Locker(sync); final long start = System.currentTimeMillis(); locker1.run(); // make sure we can serialize the synchronizer final PageAccessSynchronizer sync2 = WicketObjects.cloneObject(sync); assertTrue(sync != sync2); // make sure the clone does not retain locks by attempting to lock page locked by locker1 in // locker2 Locker locker2 = new Locker(sync2); locker2.run(); assertTrue(Duration.milliseconds(System.currentTimeMillis() - start).lessThan(timeout)); } /** https://issues.apache.org/jira/browse/WICKET-4009 */ @Test public void unlockIfNoSuchPage() { PageAccessSynchronizer synchronizer = new PageAccessSynchronizer(Duration.seconds(2)); IPageManager pageManager = new MockPageManager(); IPageManager synchronizedPageManager = synchronizer.adapt(pageManager); synchronizedPageManager.getPage(0); ConcurrentMap<Integer, PageLock> locks = synchronizer.getLocks().get(); PageLock pageLock = locks.get(Integer.valueOf(0)); assertNull(pageLock); int pageId = 1; IManageablePage page = new MockPage(pageId); synchronizedPageManager.touchPage(page); synchronizedPageManager.getPage(pageId); PageLock pageLock2 = locks.get(Integer.valueOf(pageId)); assertNotNull(pageLock2); } /** * https://issues.apache.org/jira/browse/WICKET-5316 * * @throws Exception */ @Test public void failToReleaseUnderLoad() throws Exception { final Duration duration = Duration.seconds(20); /* seconds */ final ConcurrentLinkedQueue<Exception> errors = new ConcurrentLinkedQueue<Exception>(); final long endTime = System.currentTimeMillis() + duration.getMilliseconds(); // set the synchronizer timeout one second longer than the test runs to prevent // starvation to become an issue final PageAccessSynchronizer sync = new PageAccessSynchronizer(duration.add(Duration.ONE_SECOND)); final CountDownLatch latch = new CountDownLatch(100); for (int count = 0; count < 100; count++) { new Thread() { @Override public void run() { try { while (System.currentTimeMillis() < endTime) { try { logger.debug(Thread.currentThread().getName() + " locking"); sync.lockPage(0); Thread.sleep(1); logger.debug(Thread.currentThread().getName() + " locked"); sync.unlockAllPages(); logger.debug(Thread.currentThread().getName() + " unlocked"); Thread.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } } } catch (Exception e) { logger.error(e.getMessage(), e); errors.add(e); } finally { latch.countDown(); } } }.start(); } latch.await(); if (!errors.isEmpty()) { logger.error("Number of lock errors that occurred: {}", errors.size()); throw errors.remove(); } } }
/** * Validates the response, then makes sure the timer injects itself again when called. Tests * {@link AbstractAjaxTimerBehavior#restart(AjaxRequestTarget)} method * * <p>WICKET-1525, WICKET-2152 */ public void testRestartMethod() { final Integer labelInitialValue = Integer.valueOf(0); final Label label = new Label(MockPageWithLinkAndComponent.COMPONENT_ID, new Model<Integer>(labelInitialValue)); // the duration doesn't matter because we manually trigger the behavior final AbstractAjaxTimerBehavior timerBehavior = new AbstractAjaxTimerBehavior(Duration.seconds(2)) { private static final long serialVersionUID = 1L; @Override protected void onTimer(AjaxRequestTarget target) { // increment the label's model object label.setDefaultModelObject(((Integer) label.getDefaultModelObject()) + 1); target.add(label); } }; final MockPageWithLinkAndComponent page = new MockPageWithLinkAndComponent(); page.add(label); page.add( new AjaxLink<Void>(MockPageWithLinkAndComponent.LINK_ID) { private static final long serialVersionUID = 1L; @Override public void onClick(AjaxRequestTarget target) { if (timerBehavior.isStopped()) { timerBehavior.restart(target); } else { timerBehavior.stop(target); } } }); label.setOutputMarkupId(true); label.add(timerBehavior); tester.startPage(page); final String labelPath = MockPageWithLinkAndComponent.COMPONENT_ID; // assert label == initial value (i.e. 0) tester.assertLabel(labelPath, String.valueOf(labelInitialValue)); // increment to 1 tester.executeBehavior(timerBehavior); // assert label == 1 tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 1)); // stop the timer tester.clickLink(MockPageWithLinkAndComponent.LINK_ID); // trigger it, but it is stopped tester.executeBehavior(timerBehavior); // assert label is still 1 tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 1)); // restart the timer tester.clickLink(MockPageWithLinkAndComponent.LINK_ID); // increment to 2 tester.executeBehavior(timerBehavior); // assert label is now 2 tester.assertLabel(labelPath, String.valueOf(labelInitialValue + 2)); }
/** @throws Exception */ @Test public void testConcurrency() throws Exception { runContentionTest(20, 10, Duration.seconds(10)); }
/** @throws Exception */ @Test public void testContention() throws Exception { runContentionTest(10, 20, Duration.seconds(10)); }
public void start() { if (timer == null) { timer = new Timer("Random Data Updater", true); timer.scheduleAtFixedRate(this, 0, Duration.seconds(5).getMilliseconds()); } }
/** * Behavior used to enqueue triggers and send them to the client using timer based polling. * * <p>The polling interval is configured in the constructor. The more frequent is the polling, the * more quickly your client will be updated, but also the more you will load your server and your * network. * * <p>A timeout can also be configured to indicate when the behavior should consider the page has * been disconnected. This is important to clean appropriately the resources associated with the * page. * * @author Xavier Hanin */ public class TimerChannelBehavior extends AbstractAjaxTimerBehavior implements Serializable { private static final long serialVersionUID = 1L; private static final AtomicLong COUNTER = new AtomicLong(); private static Method[] methods; private static final int ADD_COMPONENT_METHOD = 0; private static final int ADD_COMPONENT_WITH_MARKUP_ID_METHOD = 1; private static final int APPEND_JAVASCRIPT_METHOD = 2; private static final int PREPEND_JAVASCRIPT_METHOD = 3; private static final int FOCUS_COMPONENT_METHOD = 4; /** The default margin after a polling interval to consider the page is disconnected */ static final Duration TIMEOUT_MARGIN = Duration.seconds(5); static { try { methods = new Method[] { AjaxRequestTarget.class.getMethod("add", new Class[] {Component[].class}), AjaxRequestTarget.class.getMethod("add", new Class[] {Component.class, String.class}), AjaxRequestTarget.class.getMethod("appendJavaScript", new Class[] {CharSequence.class}), AjaxRequestTarget.class.getMethod( "prependJavaScript", new Class[] {CharSequence.class}), AjaxRequestTarget.class.getMethod("focusComponent", new Class[] {Component.class}), }; } catch (final Exception e) { throw new WicketRuntimeException("Unable to initialize DefaultAjaxPushBehavior", e); } } /** * This class is used to store a list of delayed method calls. * * <p>The method calls are actually calls to methods on {@link AjaxRequestTarget}, which are * invoked when the client polls the server. * * @author Xavier Hanin */ private static class DelayedMethodCallList implements Serializable { private static final long serialVersionUID = 1L; private final Application _application; /** * Used to store a method and its parameters to be later invoked on an object. * * @author Xavier Hanin */ private class DelayedMethodCall implements Serializable { private static final long serialVersionUID = 1L; /** The index of the method to invoke We store only an index to avoid serialization issues */ private final int m; /** the parameters to use when the method is called */ private final Object[] parameters; /** * Construct. * * @param m the index of the method to be called * @param parameters the parameters to use when the method is called */ public DelayedMethodCall(final int m, final Object[] parameters) { this.m = m; this.parameters = parameters; } /** * Invokes the method with the parameters on the given object. * * @see java.lang.reflect.Method#invoke(Object, Object[]) * @param o the object on which the method should be called * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InvocationTargetException */ public void invoke(final Object o) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { final Application originalApplication = Application.get(); try { ThreadContext.setApplication(_application); methods[m].invoke(o, parameters); } finally { ThreadContext.setApplication(originalApplication); } } } /** stores the list of {@link DelayedMethodCall} to invoke */ private final List<DelayedMethodCall> calls; /** Construct. */ public DelayedMethodCallList() { _application = Application.get(); calls = new ArrayList<DelayedMethodCall>(); } /** * Construct a copy of the given {@link DelayedMethodCallList}. * * @param dmcl */ public DelayedMethodCallList(final DelayedMethodCallList dmcl) { _application = Application.get(); calls = new ArrayList<DelayedMethodCall>(dmcl.calls); } /** * Add a {@link DelayedMethodCall} to the list * * @param m the index of the method to be later invoked * @param parameters the parameters to use when the method will be invoked */ public void addCall(final int m, final Object[] parameters) { calls.add(new DelayedMethodCall(m, parameters)); } /** * Invokes all the {@link DelayedMethodCall} in the list on the given Object * * @see java.lang.reflect.Method#invoke(Object, Object[]) * @param o the object on which delayed methods should be called * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InvocationTargetException */ public void invoke(final Object o) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { for (final DelayedMethodCall dmc : calls) { dmc.invoke(o); } } /** * Indicates if this list is empty or not * * @return true if this list is empty, false otherwise */ public boolean isEmpty() { return calls.isEmpty(); } /** Used to remove all the delayed methods from this list */ public void clear() { calls.clear(); } } /** * An {@link IPushTarget} implementation which enqueue {@link DelayedMethodCallList}, also called * triggers, for a {@link TimerChannelBehavior} identified by its id. * * <p>TimerPushTarget are thread safe, and can be used from any thread. Since it is not * serializable, it is not intended to be stored in a wicket component. * * @author Xavier Hanin */ public static class TimerPushTarget implements IPushTarget { /** * A trigger currently being constructed, waiting for a call to trigger to go to the triggers * list. */ private final DelayedMethodCallList currentTrigger = new DelayedMethodCallList(); /** The Wicket Application in which this target is used */ private final Application application; /** The id of the behavior to which this target corresponds */ private final String id; /** * The duration to wait before considering that a page is not connected any more This is usually * set to the polling interval + a safety margin */ private final Duration timeout; public TimerPushTarget(final Application application, final String id, final Duration timeout) { super(); this.application = application; this.id = id; this.timeout = timeout; } /** * Adds the component. * * @param component the component */ public void addComponent(final Component component) { synchronized (currentTrigger) { currentTrigger.addCall(ADD_COMPONENT_METHOD, new Object[] {component}); } } /** * Adds the component. * * @param component the component * @param markupId the markup id */ public void addComponent(final Component component, final String markupId) { synchronized (currentTrigger) { currentTrigger.addCall( ADD_COMPONENT_WITH_MARKUP_ID_METHOD, new Object[] {component, markupId}); } } /** * Append java script. * * @param javascript the javascript */ public void appendJavaScript(final String javascript) { synchronized (currentTrigger) { currentTrigger.addCall(APPEND_JAVASCRIPT_METHOD, new Object[] {javascript}); } } /** * Focus component. * * @param component the component */ public void focusComponent(final Component component) { synchronized (currentTrigger) { currentTrigger.addCall(FOCUS_COMPONENT_METHOD, new Object[] {component}); } } /** * Prepend java script. * * @param javascript the javascript */ public void prependJavaScript(final String javascript) { synchronized (currentTrigger) { currentTrigger.addCall(PREPEND_JAVASCRIPT_METHOD, new Object[] {javascript}); } } /** Trigger. */ public void trigger() { DelayedMethodCallList trigger = null; synchronized (currentTrigger) { if (currentTrigger.isEmpty()) { return; } trigger = new DelayedMethodCallList(currentTrigger); currentTrigger.clear(); } final List<DelayedMethodCallList> triggers = getTriggers(); synchronized (triggers) { triggers.add(trigger); } } public boolean isConnected() { return TimerChannelBehavior.isConnected(application, id, timeout); } /** * Methods used to access the triggers queued for the the behavior to which this target * corresponds. * * @return a List of triggers queued for the current component */ private List<DelayedMethodCallList> getTriggers() { return TimerChannelBehavior.getTriggers(application, id); } } private final String id; private final Duration timeout; /** * Construct a TimerChannelBehavior which actually refreshes the clients by polling the server for * changes at the given duration. * * @param updateInterval the interval at which the server should be polled for changes */ public TimerChannelBehavior(final Duration updateInterval) { this(updateInterval, updateInterval.add(TIMEOUT_MARGIN)); } /** * Construct a TimerChannelBehavior which actually refreshes the clients by polling the server for * changes at the given duration. * * @param updateInterval the interval at which the server should be polled for changes * @param timeout The timeout to set */ public TimerChannelBehavior(final Duration updateInterval, final Duration timeout) { super(updateInterval); id = String.valueOf(COUNTER.incrementAndGet()); this.timeout = timeout; } @Override protected void onBind() { super.onBind(); touch(getComponent().getApplication(), id); } /** @see AbstractAjaxTimerBehavior#onTimer(AjaxRequestTarget) */ @Override protected void onTimer(final AjaxRequestTarget target) { touch(getComponent().getApplication(), id); final List<DelayedMethodCallList> triggers = getTriggers(getComponent().getApplication(), id); List<DelayedMethodCallList> triggersCopy; synchronized (triggers) { if (triggers.isEmpty()) { return; } triggersCopy = new ArrayList<DelayedMethodCallList>(triggers); triggers.clear(); } for (final DelayedMethodCallList dmcl : triggersCopy) { try { dmcl.invoke(target); } catch (final Exception e) { throw new WicketRuntimeException( "a problem occured while adding events to AjaxRequestTarget", e); } } } /** * Creates a new push target to which triggers can be sent * * @return an IPushTarget to which triggers can be sent in any thread. */ public IPushTarget newPushTarget() { return new TimerPushTarget(Application.get(), id, timeout); } public void renderHead(Component component, IHeaderResponse response) { touch(getComponent().getApplication(), id); final String timerChannelPageId = getComponent().getPage().getId() + ":updateInterval:" + getUpdateInterval(); if (!getPageId(getComponent().getApplication(), id).equals(id)) { // behavior has already been redirected, we can skip this rendering return; } if (!response.wasRendered(timerChannelPageId)) { super.renderHead(component, response); setRedirectId(getComponent().getApplication(), timerChannelPageId, id); response.markRendered(timerChannelPageId); } else { /* * A similar behavior has already been rendered, we have no need to * render ourself All we need is redirect our own behavior id to the * id of the behavior which has been rendered. */ final String redirectedId = getPageId(getComponent().getApplication(), timerChannelPageId); setRedirectId(getComponent().getApplication(), id, redirectedId); } } /** Meta data key for queued triggers, stored by page behavior id */ static final MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>> TRIGGERS_KEY = new MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>>() { private static final long serialVersionUID = 1L; }; /** Meta data key for poll events time, stored by page behavior id */ static final MetaDataKey<ConcurrentMap<String, Time>> EVENTS_KEY = new MetaDataKey<ConcurrentMap<String, Time>>() { private static final long serialVersionUID = 1L; }; /** Meta data key for page behavior ids, stored by behavior id */ static final MetaDataKey<ConcurrentMap<String, String>> PAGE_ID_KEY = new MetaDataKey<ConcurrentMap<String, String>>() { private static final long serialVersionUID = 1L; }; public static boolean isConnected( final Application application, final String id, final Duration timeout) { final Time time = TimerChannelBehavior.getLastPollEvent(application, id); boolean isConnected; if (time == null) { // the behavior has been cleaned return false; } isConnected = time.elapsedSince().compareTo(timeout) < 0; if (!isConnected) { // timeout expired, the page is probably not connected anymore // we clean the metadata to avoid memory leak TimerChannelBehavior.cleanMetadata(application, id); } return isConnected; } /** * Methods used to access the triggers queued for the behavior * * <p>The implementation uses a Map stored in the application, where the behavior id is the key, * because these triggers cannot be stored in component instance or the behavior itself, since * they may be serialized and deserialized. * * @param application the application in which the triggers are stored * @param id the id of the behavior * @return a List of triggers queued for the component */ private static List<DelayedMethodCallList> getTriggers(final Application application, String id) { id = getPageId(application, id); ConcurrentMap<String, List<DelayedMethodCallList>> triggersById; synchronized (application) { triggersById = application.getMetaData(TRIGGERS_KEY); if (triggersById == null) { triggersById = new ConcurrentHashMap<String, List<DelayedMethodCallList>>(); application.setMetaData(TRIGGERS_KEY, triggersById); } } List<DelayedMethodCallList> triggers = triggersById.get(id); if (triggers == null) { triggersById.putIfAbsent(id, new ArrayList<DelayedMethodCallList>()); triggers = triggersById.get(id); } return triggers; } /** * Cleans the metadata (triggers, poll time) associated with a given behavior id * * @param application the application in which the metadata are stored * @param id the id of the behavior */ private static void cleanMetadata(final Application application, String id) { id = getPageId(application, id); ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null; ConcurrentMap<String, Time> eventsTimeById = null; ConcurrentMap<String, String> pageIdsById = null; synchronized (application) { triggersById = application.getMetaData(TRIGGERS_KEY); eventsTimeById = application.getMetaData(EVENTS_KEY); pageIdsById = application.getMetaData(PAGE_ID_KEY); } if (triggersById != null) { final List<DelayedMethodCallList> triggers = triggersById.remove(id); if (triggers != null) { synchronized (triggers) { triggers.clear(); } } } if (eventsTimeById != null) { eventsTimeById.remove(id); } if (pageIdsById != null) { pageIdsById.remove(id); } } private static void touch(final Application application, String id) { id = getPageId(application, id); ConcurrentMap<String, Time> eventsTimeById; synchronized (application) { eventsTimeById = application.getMetaData(EVENTS_KEY); if (eventsTimeById == null) { eventsTimeById = new ConcurrentHashMap<String, Time>(); application.setMetaData(EVENTS_KEY, eventsTimeById); } } eventsTimeById.put(id, Time.now()); } private static Time getLastPollEvent(final Application application, String id) { id = getPageId(application, id); ConcurrentMap<String, Time> eventsTimeById; synchronized (application) { eventsTimeById = application.getMetaData(EVENTS_KEY); if (eventsTimeById == null) { return null; } } final Time time = eventsTimeById.get(id); return time; } /** * Returns the page behavior id corresponding the given behavior id. Only one behavior is actually * rendered on a page for the same updateInterval, to optimize the number of requests. Therefore * all timer channel behaviors of the same page are redirected to the same id, using this method. * * @param application the wicket application to which the behavior belong * @param id the id of the behavior for which the page behavior id should be found * @return the page behavior id corresponding the given behavior id. */ private static String getPageId(final Application application, final String id) { ConcurrentMap<String, String> pageIdsById; synchronized (application) { pageIdsById = application.getMetaData(PAGE_ID_KEY); if (pageIdsById == null) { return id; } } final String pageId = pageIdsById.get(id); return pageId == null ? id : pageId; } private static void setRedirectId( final Application application, final String id, final String redirectedId) { ConcurrentMap<String, String> pageIdsById; synchronized (application) { pageIdsById = application.getMetaData(PAGE_ID_KEY); if (pageIdsById == null) { pageIdsById = new ConcurrentHashMap<String, String>(); application.setMetaData(PAGE_ID_KEY, pageIdsById); } } final String oldRedirectedId = pageIdsById.put(id, redirectedId); if (!redirectedId.equals(oldRedirectedId)) { /* * The id was not already redirected to the redirectedId, we need to * merge the information before redirection with information after * redirection */ final String idToRedirect = oldRedirectedId == null ? id : oldRedirectedId; redirect(application, idToRedirect, redirectedId); } } private static void redirect( final Application application, final String idToRedirect, final String redirectedId) { ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null; ConcurrentMap<String, Time> eventsTimeById = null; synchronized (application) { triggersById = application.getMetaData(TRIGGERS_KEY); eventsTimeById = application.getMetaData(EVENTS_KEY); } if (triggersById != null) { final List<DelayedMethodCallList> triggersToRedirect = triggersById.remove(idToRedirect); if (triggersToRedirect != null) { // we redirect triggers to the new list, in two steps, to avoid // acquiring // locks on two triggers simultaneously, which would be a source // of risk of // dead locks List<DelayedMethodCallList> triggersToRedirectCopy; synchronized (triggersToRedirect) { triggersToRedirectCopy = new ArrayList<DelayedMethodCallList>(triggersToRedirect); triggersToRedirect.clear(); } if (!triggersToRedirectCopy.isEmpty()) { final List<DelayedMethodCallList> triggers = getTriggers(application, redirectedId); synchronized (triggers) { triggers.addAll(triggersToRedirectCopy); } } } } if (eventsTimeById != null) { eventsTimeById.remove(idToRedirect); /* * we don't need to merge touch information, since merged behaviors * always have the same touch rates */ } } @Override public String toString() { return "TimerChannelBehavior::" + id; } public String getId() { return id; } }