Wicket serving images from File System - image

I am pretty new to Wicket and i have some difficulties with using resource references. I am using wicket 1.5.4 and have following problem: I store images on the file system. I have class ImageElement which holds part of the file path relative to configured rootFilePath (i.e dir1/dir2/img1.png). On the page I add Image as follows:
new Image("id",ImagesResourceReference.get(), pageParameters)
where page parameters includes image path parameter (path="/dir1/dir2/img1.png"). My questions are:
Is it the simplest way of serving images from the file system?
Is it ok to use ResourceReference with static method? or I should construct each time new ResourceReference? I saw that in previous version it was possible to use new ResourceReference(globalId), but it seems not to be the case anymore. If so what is the global resource reference for? So far as I understand resource reference is supposed to be factory for resources so it would be rather strange to create new factory for each resource request.
The last question is, how can i pass the path to the image in a better way so that i do not have to concatenate indexed parameters to build the path once respond method is invoked on ImageResource.
What would be the best scenario to get it working in efficient and simple way, i saw the example in 'Wicket in action', but this is meant for dynamic image generation from db and am not sure if it suites for my case
My implementation of ResourceReference which I mounted in Application under "/images" path, looks as follows:
public class ImagesResourceReference extends ResourceReference {
private static String rootFileDirectory;
private static ImagesResourceReference instance;
private ImagesResourceReference() {
super(ImagesResourceReference.class, "imagesResourcesReference");
}
public static ImagesResourceReference get() {
if(instance == null) {
if(StringUtils.isNotBlank(rootFileDirectory)) {
instance = new ImagesResourceReference();
} else {
throw new IllegalStateException("Parameter configuring root directory " +
"where images are saved is not set");
}
}
return instance;
}
public static void setRootFileDirectory(String rootFileDirectory) {
ImagesResourceReference.rootFileDirectory = rootFileDirectory;
}
private static final long serialVersionUID = 1L;
#Override
public IResource getResource() {
return new ImageResource(rootFileDirectory);
}
private static class ImageResource implements IResource {
private static final long serialVersionUID = 1L;
private final String rootFileDirectory;
public ImageResource(String rootFileDirectory) {
this.rootFileDirectory = rootFileDirectory;
}
#Override
public void respond(Attributes attributes) {
PageParameters parameters = attributes.getParameters();
List<String> indexedParams = getAllIndexedParameters(parameters);
if(!indexedParams.isEmpty() && isValidImagePath(indexedParams)) {
String pathToRequestedImage = getImagePath(indexedParams);
FileResourceStream fileResourceStream = new FileResourceStream(new File(pathToRequestedImage));
ResourceStreamResource resource = new ResourceStreamResource(fileResourceStream);
resource.respond(attributes);
}
}
private boolean isValidImagePath(List<String> indexedParams) {
String fileName = indexedParams.get(indexedParams.size() -1);
return !FilenameUtils.getExtension(fileName).isEmpty();
}
private List<String> getAllIndexedParameters(PageParameters parameters) {
int indexedparamCount = parameters.getIndexedCount();
List<String> indexedParameters = new ArrayList<String>();
for(int i=0; i<indexedparamCount ;i++) {
indexedParameters.add(parameters.get(i).toString());
}
return indexedParameters;
}
private String getImagePath(List<String> indexedParams) {
return rootFileDirectory + File.separator + StringUtils.join(indexedParams, File.separator);
}
}
Any help and advices appreciated! Thanks in advance.

You could use it as a shared resource:
public class WicketApplication extends WebApplication {
#Override
public Class<HomePage> getHomePage() {
return HomePage.class;
}
#Override
public void init() {
super.init();
getSharedResources().add("downloads", new FolderContentResource(new File("C:\\Users\\ronald.tetsuo\\Downloads")));
mountResource("downloads", new SharedResourceReference("downloads"));
}
static class FolderContentResource implements IResource {
private final File rootFolder;
public FolderContentResource(File rootFolder) {
this.rootFolder = rootFolder;
}
public void respond(Attributes attributes) {
PageParameters parameters = attributes.getParameters();
String fileName = parameters.get(0).toString();
File file = new File(rootFolder, fileName);
FileResourceStream fileResourceStream = new FileResourceStream(file);
ResourceStreamResource resource = new ResourceStreamResource(fileResourceStream);
resource.respond(attributes);
}
}
}

You can still use ResourceReferences with global IDs. You just have to use a SharedResourceReference. This is probably better, too.
add(new Image("image", new SharedResourceReference("mySharedResourceRef", parameters));
I would try to avoid building paths from URL parameters. This can easily end up in security leaks.

Related

wicket; images from file system. I added resourse folder, and still working

What I want is adding resource folder from file system for storing images and displaying them. I added the folder in the Application, and still working.
WicketTestApplication#init
getResourceSettings().getResourceFinders().add( new WebApplicationPath( getServletContext(), "C:\\image" ) );
And TestPage
public class TestPage extends WebPage {
private static final long serialVersionUID = 1L;
public TestPage() {
add( new ContextImage( "image", "C:/image/rhodes.jpg" ) );
}
}
Do I miss something?
WebApplicationPath is a IResourceFinder that will look for resources in the web application path, except in WEB-INF/ folder. So you cannot use it to load something from your file system.
I'd suggest you to use FileSystemResource[Reference] instead or a specialization of DynamicImageResource.
private static class ImageResource extends DynamicImageResource {
#Override
protected byte[] getImageData(Attributes attributes) {
PageParameters parameters = attributes.getParameters();
StringValue name = parameters.get("name");
byte[] imageBytes = null;
if (name.isEmpty() == false) {
imageBytes = getImageAsBytes(name.toString());
}
return imageBytes;
}
private byte[] getImageAsBytes(String imageName) {
// read the image from the file system, e.g. with FileInputStream(folder, imageName);
}
#Override
public boolean equals(Object that) {
return that instanceof ImageResource;
}
}
An article explaining this approach can be found at: http://wicketinaction.com/2011/07/wicket-1-5-mounting-resources/

Cucumber Guice / Injector seems not to be thread-safe (Parallel execution / ExecutorService)

[long description warning]
I'm running some cucumber tests which have to be executed intercalated a defined server - for instance:
a.feature -> JBoss Server 1 | b.feature -> JBoss Serv. 2 | c.feature -> JB1 | etc.
For that, I created a hypothetical ExecutorService like this:
final ExecutorService executorService = Executors.newFixedThreadPool(2); //numberOfServers
for (Runnable task : tasks) {
executorService.execute(task);
}
executorService.shutdown();
try {
executorService.awaitTermination(1000, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//doX();
}
The way that I manage about how will be the server chosen as liable to execute is:
inside of my Runnable class created for the executorService, I pass as a parameter a instanceId to a TestNG (XmlTest class) as below:
#Override
public void run() {
setupTest().run();
}
private TestNG setupTest() {
TestNG testNG = new TestNG();
XmlSuite xmlSuite = new XmlSuite();
XmlTest xmlTest = new XmlTest(xmlSuite);
xmlTest.setName(//irrelevant);
xmlTest.addParameter("instanceId", String.valueOf(instanceId));
xmlTest.setXmlClasses(..........);
testNG.setXmlSuites(..........);
return testNG;
}
Then, I get this just fine in a class that extends TestNgCucumberAdaptor:
#BeforeTest
#Parameters({"instanceId"})
public void setInstanceId(#Optional("") String instanceId) {
if (!StringUtils.isEmpty(instanceId)) {
this.instanceId = Integer.valueOf(instanceId);
}
}
And inside a #BeforeClass I'm populating a Pojo with this instanceId and setting the Pojo in a threadLocal attribute of another class. So far, so good.
public class CurrentPojoContext {
private static final ThreadLocal<PojoContext> TEST_CONTEXT = new ThreadLocal<PojoContext>();
...
public static PojoContext getContext(){
TEST_CONTEXT.get();
}
Now the problem really starts - I'm using Guice (Cucumber guice as well) in a 3rd class, injecting this pojo object that contains the instanceId. The example follows:
public class Environment {
protected final PojoContext pojoContext;
#Inject
public Environment() {
this.pojoContext = CurrentPojoContext.getContext();
}
public void foo() {
print(pojoContext.instanceId); // output: 1
Another.doSomething(pojoContext);
}
class Another{
public String doSomething(PojoContext p){
print(p.instanceId); // output: 2
}
}
}
Here it is not every time like this the outputs (1 and 2) but from time to time, I realized that the execution of different threads is messing with the attribute pojoContext. I know that is a little confusing, but my guess is that the Guice Injector is not thread-safe for this scenario - it might be a long shot, but I'd appreciate if someone else takes a guess.
Regards
Well, just in order to provide a solution for someone else, my solution was the following:
Create a class that maintains a Map with an identifier (unique and thread-safe one) as the key and a Guice Injector as value;
Inside my instantiation of Guice injector, I created my own module
Guice.createInjector(Stage.PRODUCTION, MyOwnModules.SCENARIO, new RandomModule());
and for this module:
public class MyOwnModules {
public static final Module SCENARIO = new ScenarioModule(MyOwnCucumberScopes.SCENARIO);
}
the scope defined here provides the following:
public class MyOwnCucumberScopes {
public static final ScenarioScope SCENARIO = new ParallelScenarioScope();
}
To sum up, the thread-safe will be in the ParallelScenarioScope:
public class ParallelScenarioScope implements ScenarioScope {
private static final Logger LOGGER = Logger.getLogger(ParallelScenarioScope.class);
private final ThreadLocal<Map<Key<?>, Object>> threadLocalMap = new ThreadLocal<Map<Key<?>, Object>>();
#Override
public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
return new Provider<T>() {
public T get() {
Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
#SuppressWarnings("unchecked")
T current = (T) scopedObjects.get(key);
if (current == null && !scopedObjects.containsKey(key)) {
current = unscoped.get();
scopedObjects.put(key, current);
}
return current;
}
};
}
protected <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
Map<Key<?>, Object> map = threadLocalMap.get();
if (map == null) {
throw new OutOfScopeException("Cannot access " + key + " outside of a scoping block");
}
return map;
}
#Override
public void enterScope() {
checkState(threadLocalMap.get() == null, "A scoping block is already in progress");
threadLocalMap.set(new ConcurrentHashMap<Key<?>, Object>());
}
#Override
public void exitScope() {
checkState(threadLocalMap.get() != null, "No scoping block in progress");
threadLocalMap.remove();
}
private void checkState(boolean expression, String errorMessage) {
if (!expression) {
LOGGER.info("M=checkState, Will throw exception: " + errorMessage);
throw new IllegalStateException(errorMessage);
}
}
}
Now the gotcha is just to be careful regarding the #ScenarioScoped and the code will work as expected.

Customize SLF4J Logger

I'm trying to find a nice way to add a prefix to my logs without passing it on every calls, without instanciate Logger again.
The purpose is to trace Rest calls individually.
(The prefix would be re-generated on each call using UUID)
This would be like
#RestController
class MyClass {
//Here the prefix is initialise once
//default value is X
Logger LOG = LoggerFactory.getLogger(MyClass.class);
#RequestMapping("/a")
void methodA() {
LOG.debug("foo");
}
#RequestMapping("/b")
void methodB() {
LOG.setPrefix("B");
LOG.debug("bar");
}
with this output
[...] [prefix X] foo
[...] [prefix B] bar
As you've said you're using Logback, here's a couple options to do the kind of thing you're trying to do:
Markers
Each log entry can have a "marker" established for it. (The best documentation I've seen for it is in the SLF4J FAQ.) Something like:
class MyClass {
Marker methodBMarker = MarkerFactory.getMarker("B");
Logger logger = LoggerFactory.getLogger(MyClass.class);
…
void methodB() {
logger.debug(methodBMarker, "bar");
}
}
You would need to update all log entries in each method to use the appropriate marker. You can then put %marker in your layout to put the log entry's marker into the log.
MDC
The other option is to use the "Mapped Diagnostic Context" functionality to specify the current "context" for each log entry.
class MyClass {
Logger logger = LoggerFactory.getLogger(MyClass.class);
…
void methodB() {
MDC.put("method", "b");
try {
…
logger.debug("bar");
…
} finally {
MDC.clear();
}
}
}
You would then use %mdc{method} in your layout to output that particular MDC value. Note that MDC is really intended to be used for per-thread values like something web-connection-specific, so it's important to ensure that it's cleared out of what you don't want when you're leaving the context you want the value logged in.
Please see http://www.slf4j.org/extensions.html#event_logger for an example of how to use the MDC. You do not have to use the EventLogger. Once you set things in the MDC they are present in every log record.
A Marker does not meet your criteria since it has to be specified on every call.
Here's my MDC implementation explained to share my experiments with MDC.
//In this abstract class i'm defining initLogData methods to set MDC context
//It would be inherited by Controller and other classes who needs logging with traced transactions
public abstract class AbstractService {
protected LogData initLogData() {
return LogData.init();
}
protected LogData initLogData(String tName) {
return LogData.init(tName);
}
}
//LogData holds the MDC logic
public class LogData {
private final static int nRandom = 8;
//this keys are defined in logback pattern (see below)
private final static String tIdKey = "TID";
private final static String tNameKey = "TNAME";
//Transaction id
private String tId;
//Transaction name
private String tName;
public String getTId() {
return tId;
}
public void setTId(String tId) {
this.tId = tId;
}
public String gettName() {
return tName;
}
public void settName(String tName) {
this.tName = tName;
}
//random transaction id
//I'm not using uuid since its too longs and perfect unicity is not critical here
public String createTId(){
Random r = new Random();
StringBuilder sb = new StringBuilder();
while(sb.length() < nRandom){
sb.append(Integer.toHexString(r.nextInt()));
}
return sb.toString().substring(0, nRandom);
}
//private constructors (use init() methods to set LogData)
private LogData(String tId, String tName) {
this.tId = tId;
this.tName = tName;
}
private LogData(String tName) {
this.tId = createTId();
this.tName = tName;
}
private LogData() {
this.tId = createTId();
}
//init MDC with cascading calls processing (using same id/name within same context
//even if init() is called again)
public static LogData init(String tName) {
String previousTId = MDC.get(tIdKey);
String previousTName = MDC.get(tNameKey);
MDC.clear();
LogData logData = null;
if(previousTId != null) {
logData = new LogData(previousTId, previousTName);
} else {
logData = new LogData(tName);
}
MDC.put(tIdKey, logData.getTId());
MDC.put(tNameKey, logData.gettName());
return logData;
}
//init MDC without cascading calls management (new keys are generated for each init() call)
public static LogData init() {
MDC.clear();
LogData logData = new LogData();
MDC.put(tIdKey, logData.getTId());
return logData;
}
}
//logback.xml : values to include in log pattern
[%X{TID}] [%X{TNAME}]
#RestController
#RequestMapping("/test")
public class RestControllerTest extends AbstractRestService {
private final Logger LOG = LoggerFactory.getLogger(ServiceRestEntrypointStatus.class);
#RequestMapping(value="/testA")
public void testA() {
initLogData("testA");
LOG.debug("This is A");
}
#RequestMapping(value="/testB")
public void testB() {
initLogData("testA");
LOG.debug("This is B");
}
#RequestMapping(value="/testC")
public void testC() {
initLogData("testC");
LOG.debug("This is C");
testA();
testB();
}
}
Calling RestControllerTest mapped /test/testA produces :
[fdb5d310] [testA] This is A
Calling /test/testC produces (id and name are kept even if initLogData is called in sub methods):
[c7b0af53] [testC] This is C
[c7b0af53] [testC] This is A
[c7b0af53] [testC] This is B

How to implement custom SftpSimplePatternFileListFilter?

I am newbee to Spring integration. i am trying to implement customer sftp filter to list the files in SFTP server. I am getting "The blank final field seen may not have been initialized" at the constructor.Can you please suggest me to get list of file names from sftp server.
I dont have any idea what went wrong in my code.
Thanks in Advance
java code
public class SFTPFileFilter extends SftpSimplePatternFileListFilter {
public SFTPFileFilter(String pattern) {
super(pattern);
// TODO Auto-generated constructor stub
}
final static Logger logger = LoggerFactory.getLogger(SFTPFileFilter.class);
private final Queue<File> seen;
private final Set<File> seenSet = new HashSet<File>();
private final Object monitor = new Object();
public static int fileCount = 0;
#Autowired
private SourcePollingChannelAdapter sftpInbondAdapter;
public List<File> filterFiles(File[] files)
{
List<File> accepted = new ArrayList<File>();
for (File file : files) {
System.out.println(file.getName());
accepted.add(file);
}
return accepted;
}
public boolean accept(File file) {
synchronized (this.monitor) {
if (this.seenSet.contains(file)) {
logger.info(file.getName()+" is already copied earlier");
return false;
}
if (this.seen != null) {
if (!this.seen.offer(file)) {
File removed = this.seen.poll();
this.seenSet.remove(removed);
this.seen.add(file);
}
}
this.seenSet.add(file);
return true;
}
}
}
private final Queue<File> seen;
You are not initializing that field in a constructor.
You can't extend it like that; simply override the method like this...
public List<File> filterFiles(File[] files) {
for (File file : files) {
System.out.println("received:" + file.getName());
}
List<File> filtered = super.filterFiles(files);
for (File file : flteredFiles) {
System.out.println("after filter:" + file.getName());
}
return filteredFiles;
}

Wicket why page expires when opening link in new tab?

I'm building a wicket bootsrap web application with the following specs (from pom.xml):
wicket version: 6.15.0
wicket-bootstrap-core.version: 0.9.3-SNAPSHOT
I have a base page which is the father of my other pages and adds to mark up a horizontal navigation bar on top, with key component:
BootstrapBookmarkablePageLink extends BookmarkablePageLink
This is part of my BasePage.java
public abstract class BasePage extends GenericWebPage<Void> {
private static final long serialVersionUID = 1L;
String username;
public WicketApplication getApp() {
return WicketApplication.class.cast(getApplication());
}
public BasePage(final PageParameters parameters) {
super(parameters);
// Read session data
cachedUsername = (String)
BasicAuthenticationSession.get().getAttribute("username");
// create navbar
add(newNavbar("navbar"));
}
/**
* #return application properties
*/
public Properties getProperties() {
return WicketApplication.get().getProperties();
}
/**
* creates a new {#link Navbar} instance
*
* #param markupId
* The components markup id.
* #return a new {#link Navbar} instance
*/
protected Navbar newNavbar(String markupId) {
Navbar navbar = new Navbar(markupId) {
private static final long serialVersionUID = 1L;
#Override
protected TransparentWebMarkupContainer newCollapseContainer(String
componentId) {
TransparentWebMarkupContainer container =
super.newCollapseContainer(componentId);
container.add(new CssClassNameAppender("bs-navbar-collapse"));
return container;
}
};
navbar.setPosition(Navbar.Position.TOP);
// navbar.setInverted(true);
NavbarButton<Void> myTab = new NavbarButton<Void>(MyPage.class, new
PageParameters().add("name", "")
.add("status", "All").add("date", ""), Model.of("My page"));
NavbarButton<Void> myOtherTab = new NavbarButton<Void>
(MyOtherPage.class, new PageParameters().add("status", "initial")
.add("date", ""), Model.of("My other page"));
navbar.addComponents(NavbarComponents.transform(
Navbar.ComponentPosition.LEFT,
myTab, myOtherTab));
return navbar;
}
}
Then, MyPage renders a filter form, an html table, ajaxbuttons and some links, Some of my components are ajax components:
public class MyPage extends BasePage {
private static final long serialVersionUID = 5772520351966806522L;
#SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(MyPage.class);
private static final Integer DAYS = 270;
private DashboardFilteringPageForm filteringForm;
private CityInitialForm ncForm;
private String CityName;
private String startDate;
private CitysTablePanel citysTable;
private WebMarkupContainer numberOfNodes;
public MyPage(PageParameters parameters) throws ParseException {
super(parameters);
// get Citys list from repo
final List<City> repoCitys = (List<City>) methodToGetCities();
// select number of nodes
numberOfNodes = new WebMarkupContainer("numberOfNodes") {
private static final long serialVersionUID = 5772520351966806522L;
};
numberOfNodes.setOutputMarkupId(true);
ncForm = new CityInitialForm("ncForm");
// validation
add(new FeedbackPanel("feedbackPanel")).setOutputMarkupId(true);
ncForm.getNumberField().setRequired(true);
ncForm.add(new AjaxButton("ncButton") {
private static final long serialVersionUID = -6846211690328190809L;
#Override
protected void onInitialize() {
super.onInitialize();
add(newAjaxFormSubmitBehavior("change"));
}
#Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
// redirect to other page
}
#Override
protected void updateAjaxAttributes(AjaxRequestAttributes
attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new
DisableComponentListener(citysTable));
}
});
numberOfNodes.add(ncForm);
// filters
CityName = parameters.get("name").toString() == null ? "" :
parameters.get("name").toString();
startDate = parameters.get("date").toString();
filteringForm = new DashboardFilteringPageForm("filteringForm") {
private static final long serialVersionUID = -1702151172272765464L;
};
// initialize form inputs
filteringForm.setCityName(CityName);
try {
filteringForm.setStartDate(new SimpleDateFormat("EE MMM dd HH:mm:ss
z yyyy", Locale.ENGLISH)
.parse(getStartDate().equals("") ?
CortexWebUtil.subtractDays(new Date(), DAYS).toString() : getStartDate()));
} catch (Exception e) {
setResponsePage(SignInPage.class, new PageParameters());
}
filteringForm.add(new AjaxButton("button") {
private static final long serialVersionUID = -6846211690328190809L;
#Override
protected void onInitialize() {
super.onInitialize();
add(newAjaxFormSubmitBehavior("change"));
}
#Override
protected void onSubmit(AjaxRequestTarget target, Form<?> paForm) {
// retrieve Citys
filterCitysAjax(target, "All");
}
#Override
protected void updateAjaxAttributes(AjaxRequestAttributes
attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new
DisableComponentListener(citysTable));
}
});
filteringForm.getCityNameTextField().add(new OnChangeAjaxBehavior() {
private static final long serialVersionUID = 1468056167693038096L;
#Override
protected void onUpdate(AjaxRequestTarget target) {
try {
filterCitysAjax(target, "All");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
#Override
protected void updateAjaxAttributes(AjaxRequestAttributes
attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new
DisableComponentListener(citysTable));
}
});
// new City link
AjaxLink<Void> newCityLink = newCityLink("newCity", repoCitys);
// Citys table
citysTable = new CitysTablePanel("CitysTable", repoCitys);
citysTable.setOutputMarkupId(true);
// add components
add(filteringForm, newCityLink, numberOfNodes, citysTable);
}
private void filterCitysAjax(AjaxRequestTarget target, String status) {
methodToFilterResults();
// re-render table component
CitysTablePanel cityTableNew = new CitysTablePanel("CitysTable", citys);
cityTableNew.setOutputMarkupId(true);
cityTableNew.setVisibilityAllowed(true);
cityTableNew.setVisible(true);
citysTable.replaceWith(cityTableNew);
target.add(cityTableNew);
citysTable = cityTableNew;
target.appendJavaScript(CortexWebUtil.TABLE_ODD_EVEN_ROWS);
}
private AjaxLink<Void> newCityLink(String string, final List<City> Citys) {
final AjaxLink<Void> newCityLink = new AjaxLink<Void>(string) {
private static final long serialVersionUID = -5420108740617806989L;
#Override
public void onClick(final AjaxRequestTarget target) {
numberOfNodes.add(new AttributeModifier("style",
"display:block"));
target.add(numberOfNodes);
}
};
// new City image
Image newCityImage = new Image("newCityIcon", new
ContextRelativeResource("/img/new_City_icon.png"));
add(newCityLink);
newCityLink.add(newCityImage);
return newCityLink;
}
}
So MyPage works but when I open MyOtherPage Link in an a new tab and trigger an ajax component in MyPage (e.g the AjaxButton) then I get the page expirtaion error.
Why is that happening?
Do I need to use stateless pages? ( stateless link )
Why would it be so ard in wicket to open links in new tabs and use ajax components? I must be missing sometthing..
Here are few possible reasons:
MyPage fails to serialize
Wicket stores stateful pages in page storage (in the disk, by default). Later when you click a stateful link Wicket tries to load the page. First it looks in the http session where the page is kept in its live form (i.e. not serialized). If it is not found there then Wicket looks in the disk.
Wicket keeps only the page(s) used in the last user request in the Http Session (to keep memory footprint small). By clicking on MyOtherPage link you put an instance of MyOtherPage in the Http session and the old instance (of MyPage) is only in the disk. But: if MyPage fails to serialize to byte[] then it cannot be stored in the disk and thus later requests will fail with PageExpiredException.
Todo: Check your logs for NotSerializableException with nice debug message of the reason.
MyOtherPage is too big
By default Wicket writes up to 10M per user session in the disk. If MyPage is let's say 2M and MyOtherPage is 9M (both sizes are quite big, but I don't know what happens in your app...) then saving MyOtherPage will remove MyPage from the disk. Later attempts to load MyPage will fail with PageExpiredException.
Todo: Review your usage of Wicket Models.

Resources