Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2000-2018 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.cdi.itest.sessioncontextspecializes;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.spi.Contextual;
import jakarta.enterprise.inject.Specializes;

import java.util.concurrent.atomic.AtomicReference;

import com.vaadin.cdi.context.VaadinSessionScopedContext;
import com.vaadin.cdi.util.ContextualStorage;
import com.vaadin.flow.server.VaadinSession;

/**
* Replaces the framework-default
* {@link VaadinSessionScopedContext.ContextualStorageManager} via
* {@code @Specializes} to activate the {@code @VaadinSessionScoped} context
* whenever a {@link VaadinSession} is set on the current thread, and to acquire
* the session lock around storage access when the calling thread does not
* already hold it.
*/
@ApplicationScoped
@Specializes
public class LenientSessionContextManager
extends VaadinSessionScopedContext.ContextualStorageManager {

@Override
protected boolean isActive() {
return VaadinSession.getCurrent() != null;
}

@Override
protected ContextualStorage getContextualStorage(Contextual<?> contextual,
boolean createIfNotExist) {
VaadinSession session = VaadinSession.getCurrent();
if (session.hasLock()) {
return super.getContextualStorage(contextual, createIfNotExist);
}
AtomicReference<ContextualStorage> ref = new AtomicReference<>();
session.accessSynchronously(() -> ref
.set(super.getContextualStorage(contextual, createIfNotExist)));
return ref.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2000-2018 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.cdi.itest.sessioncontextspecializes;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.enterprise.event.Event;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import com.vaadin.cdi.annotation.CdiComponent;
import com.vaadin.cdi.annotation.VaadinSessionScoped;
import com.vaadin.cdi.itest.Counter;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinSession;

/**
* Background-thread probe used by both strict-default and {@code @Specializes}
* integration tests. From a thread that has only set the {@link VaadinSession}
* thread-local — without acquiring the session lock — the view calls a method
* on a {@code @VaadinSessionScoped} bean and fires a CDI event observed by the
* same bean.
* <p>
* Outcomes are recorded via the application-scoped {@link Counter} so that
* failures on the session-scoped proxy don't lose information:
* {@link #DIRECT_CALL_COUNT} is incremented if the proxy call succeeds,
* {@link #OBSERVED_COUNT} if the observer is invoked, and {@link #ERROR_COUNT}
* for each {@link RuntimeException} caught by the background thread.
* {@link #DONE_COUNT} is incremented when the background thread finishes,
* regardless of outcome — tests synchronize on it before asserting the other
* counters.
*/
@Route("")
@CdiComponent
public class SessionContextSpecializesView extends Div {

public static final String FIREBTN_ID = "firebtn";
public static final String OBSERVED_COUNT = "specializesObserved";
public static final String DIRECT_CALL_COUNT = "specializesDirectCall";
public static final String ERROR_COUNT = "specializesError";
public static final String UNEXPECTED_ERROR_COUNT = "specializesUnexpectedError";
public static final String DONE_COUNT = "specializesBackgroundDone";

@Inject
private SessionScopedObserver observer;

@Inject
private Event<BackgroundEvent> eventBus;

@Inject
private Counter counter;

@PostConstruct
private void init() {
NativeButton fireBtn = new NativeButton("fire-background");
fireBtn.addClickListener(click -> {
VaadinSession session = VaadinSession.getCurrent();
Thread thread = new Thread(() -> {
VaadinSession previous = VaadinSession.getCurrent();
VaadinSession.setCurrent(session);
try {
try {
// Touch a @VaadinSessionScoped bean directly from a
// background thread that does NOT hold the session
// lock.
observer.recordDirectCall();
} catch (ContextNotActiveException e) {
counter.increment(ERROR_COUNT);
} catch (RuntimeException e) {
counter.increment(UNEXPECTED_ERROR_COUNT);
}
try {
// Fire a CDI event; the observer is on a
// @VaadinSessionScoped bean.
eventBus.fire(new BackgroundEvent());
} catch (ContextNotActiveException e) {
counter.increment(ERROR_COUNT);
} catch (RuntimeException e) {
counter.increment(UNEXPECTED_ERROR_COUNT);
}
} finally {
VaadinSession.setCurrent(previous);
// Completion signal: tests wait for this counter before
// asserting the other ones.
counter.increment(DONE_COUNT);
}
}, "background-event-fire");
thread.start();
});
fireBtn.setId(FIREBTN_ID);
add(fireBtn);
}

public static class BackgroundEvent {
}

@VaadinSessionScoped
public static class SessionScopedObserver {

@Inject
private Counter counter;

public void recordDirectCall() {
counter.increment(DIRECT_CALL_COUNT);
}

private void onBackgroundEvent(@Observes BackgroundEvent event) {
counter.increment(OBSERVED_COUNT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.URL;

import org.jboss.arquillian.container.test.api.RunAsClient;
Expand Down Expand Up @@ -76,6 +77,16 @@ protected void assertCountEquals(int expectedCount, String counter)
Assert.assertEquals(expectedCount, getCount(counter));
}

protected void waitForCount(int expectedCount, String counter) {
waitUntil(driver -> {
try {
return getCount(counter) == expectedCount;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, 10);
}

protected void assertTextEquals(String expectedText, String elementId) {
Assert.assertEquals(expectedText, getText(elementId));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2000-2018 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.cdi.itest;

import java.io.IOException;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.cdi.itest.sessioncontextspecializes.LenientSessionContextManager;
import com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView;

import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DIRECT_CALL_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DONE_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.ERROR_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.FIREBTN_ID;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.OBSERVED_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.UNEXPECTED_ERROR_COUNT;

/**
* Verifies that an application can take {@code @VaadinSessionScoped} beans into
* use from a background thread that only sets the
* {@link com.vaadin.flow.server.VaadinSession} thread-local — without holding
* the session lock — by providing a {@code @Specializes} lenient
* {@link com.vaadin.cdi.context.VaadinSessionScopedContext.ContextualStorageManager}.
* <p>
* Reproduces the user-facing scenarios from issues #495 and #506.
*/
public class SessionContextSpecializesTest extends AbstractCdiTest {

@Deployment(testable = false)
public static WebArchive deployment() {
return ArchiveProvider.createWebArchive("session-context-specializes",
SessionContextSpecializesView.class,
SessionContextSpecializesView.BackgroundEvent.class,
SessionContextSpecializesView.SessionScopedObserver.class,
LenientSessionContextManager.class);
}

@Before
public void setUp() throws Exception {
resetCounts();
open();
}

@Test
public void backgroundThread_firesEventAndCallsBean_specializesAllowsIt()
throws IOException {
assertCountEquals(0, OBSERVED_COUNT);
assertCountEquals(0, DIRECT_CALL_COUNT);
assertCountEquals(0, ERROR_COUNT);
assertCountEquals(0, UNEXPECTED_ERROR_COUNT);

click(FIREBTN_ID);

// The background thread updates the counters asynchronously; wait
// for its completion signal before asserting.
waitForCount(1, DONE_COUNT);

assertCountEquals(1, DIRECT_CALL_COUNT);
assertCountEquals(1, OBSERVED_COUNT);
assertCountEquals(0, ERROR_COUNT);
assertCountEquals(0, UNEXPECTED_ERROR_COUNT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2000-2018 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.cdi.itest;

import java.io.IOException;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView;

import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DIRECT_CALL_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DONE_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.ERROR_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.FIREBTN_ID;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.OBSERVED_COUNT;
import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.UNEXPECTED_ERROR_COUNT;
import static org.junit.Assert.assertTrue;

/**
* Verifies the framework's supported activation condition for
* {@code @VaadinSessionScoped}: when the current thread has set the
* {@link com.vaadin.flow.server.VaadinSession} thread-local but does not hold
* the session lock, the context is inactive — bean lookups fail and observer
* methods on session-scoped beans are not invoked. Regression guard for the
* lock check introduced in {@code VaadinSessionScopedContext.isActive()}.
*/
public class SessionContextStrictTest extends AbstractCdiTest {

@Deployment(testable = false)
public static WebArchive deployment() {
return ArchiveProvider.createWebArchive("session-context-strict",
SessionContextSpecializesView.class,
SessionContextSpecializesView.BackgroundEvent.class,
SessionContextSpecializesView.SessionScopedObserver.class);
}

@Before
public void setUp() throws Exception {
resetCounts();
open();
}

@Test
public void backgroundThread_withoutSpecializes_contextInactive()
throws IOException {
assertCountEquals(0, OBSERVED_COUNT);
assertCountEquals(0, DIRECT_CALL_COUNT);
assertCountEquals(0, ERROR_COUNT);
assertCountEquals(0, UNEXPECTED_ERROR_COUNT);

click(FIREBTN_ID);

// The background thread updates the counters asynchronously; wait
// for its completion signal so the zero-assertions below are
// meaningful instead of passing on a not-yet-run thread.
waitForCount(1, DONE_COUNT);

// Neither the proxy call nor the observer dispatch succeeded:
assertCountEquals(0, DIRECT_CALL_COUNT);
assertCountEquals(0, OBSERVED_COUNT);
// At least one of the two attempts threw ContextNotActiveException
// because the context is inactive on an unlocked thread.
assertTrue("Expected at least one ContextNotActiveException, got "
+ getCount(ERROR_COUNT), getCount(ERROR_COUNT) >= 1);
// Any other RuntimeException would indicate something went wrong
// unrelated to the inactive context — guard against false positives.
assertCountEquals(0, UNEXPECTED_ERROR_COUNT);
}
}
Loading
Loading