You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by mg...@apache.org on 2012/03/07 09:39:03 UTC
[5/19] WICKET-4439 Move classes around so that there are no two
packages with the same name in different modules
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
new file mode 100644
index 0000000..1525420
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.util.ClassProvider;
+
+/**
+ * A mapper that is used when a request to the home page ("/") is made
+ */
+public class HomePageMapper extends MountedMapper
+{
+
+ /**
+ * Construct.
+ *
+ * @param pageClass
+ * the class of the page which should handle requests to "/"
+ */
+ public HomePageMapper(final Class<? extends IRequestablePage> pageClass)
+ {
+ super("/", pageClass);
+ }
+
+ /**
+ * Construct.
+ *
+ * @param pageClassProvider
+ * the class of the page which should handle requests to "/"
+ */
+ public HomePageMapper(ClassProvider<? extends IRequestablePage> pageClassProvider)
+ {
+ super("/", pageClassProvider);
+ }
+
+ /**
+ * Construct.
+ *
+ * @param pageClass
+ * the class of the page which should handle requests to "/"
+ * @param pageParametersEncoder
+ * the encoder that will be used to encode/decode the page parameters
+ */
+ public HomePageMapper(Class<? extends IRequestablePage> pageClass,
+ IPageParametersEncoder pageParametersEncoder)
+ {
+ super("/", pageClass, pageParametersEncoder);
+ }
+
+ /**
+ * Construct.
+ *
+ * @param pageClassProvider
+ * the class of the page which should handle requests to "/"
+ * @param pageParametersEncoder
+ * the encoder that will be used to encode/decode the page parameters
+ */
+ public HomePageMapper(final ClassProvider<? extends IRequestablePage> pageClassProvider,
+ IPageParametersEncoder pageParametersEncoder)
+ {
+ super("/", pageClassProvider, pageParametersEncoder);
+ }
+
+ /**
+ * Matches only when there are no segments/indexed parameters
+ *
+ * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+ */
+ @Override
+ protected UrlInfo parseRequest(Request request)
+ {
+ // get canonical url
+ final Url url = request.getUrl().canonical();
+
+ if (url.getSegments().size() > 0)
+ {
+ // home page cannot have segments/indexed parameters
+ return null;
+ }
+
+ return super.parseRequest(request);
+ }
+
+ /**
+ * Use this mapper as a last option. Let all other mappers to try to handle the request
+ *
+ * @see org.apache.wicket.request.mapper.MountedMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+ */
+ @Override
+ public int getCompatibilityScore(Request request)
+ {
+ return Integer.MIN_VALUE + 1;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
new file mode 100644
index 0000000..ec8f51f
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.resource.ResourceReferenceRegistry;
+
+/**
+ * Utility interface for providing and creating new page instances.
+ *
+ * @author Matej Knopp
+ */
+public interface IMapperContext extends IPageSource
+{
+ /**
+ * @return the namespace for Wicket URLs.
+ */
+ String getNamespace();
+
+ /**
+ * @return identifier for non bookmarkable URLs
+ */
+ String getPageIdentifier();
+
+ /**
+ * @return identifier for bookmarkable URLs
+ */
+ String getBookmarkableIdentifier();
+
+ /**
+ * @return identifier for resources
+ */
+ String getResourceIdentifier();
+
+ /**
+ * @return {@link ResourceReferenceRegistry}
+ */
+ ResourceReferenceRegistry getResourceReferenceRegistry();
+
+ /**
+ * Returns the listener interface name as string.
+ *
+ * @param listenerInterface
+ * @return listener interface name as string
+ */
+ String requestListenerInterfaceToString(RequestListenerInterface listenerInterface);
+
+ /**
+ * Returns listener interface for the name
+ *
+ * @param interfaceName
+ * @return listener interface
+ */
+ RequestListenerInterface requestListenerInterfaceFromString(String interfaceName);
+
+ /**
+ * Returns the home page class.
+ *
+ * @return home page class
+ */
+ Class<? extends IRequestablePage> getHomePageClass();
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
new file mode 100644
index 0000000..2f6c93b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * Interface for objects that are capable of getting and creating page instance.
+ *
+ * @author Matej Knopp
+ */
+public interface IPageSource
+{
+ /**
+ * Returns existing page instance if the page exists.
+ *
+ * @param pageId
+ * @return page instance or <code>null</code> if the page does not exist.
+ */
+ IRequestablePage getPageInstance(int pageId);
+
+ /**
+ * Creates new page instance of page with given class. The page should be marked as create
+ * bookmarkable, so subsequent calls to {@link IRequestablePage#wasCreatedBookmarkable()} must
+ * return <code>true</code>
+ *
+ * @param pageClass
+ * @param pageParameters
+ * @return new page instance
+ */
+ IRequestablePage newPageInstance(Class<? extends IRequestablePage> pageClass,
+ PageParameters pageParameters);
+
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
new file mode 100644
index 0000000..118bfb5
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
@@ -0,0 +1,524 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.info.ComponentInfo;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.info.PageInfo;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
+import org.apache.wicket.util.ClassProvider;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * Encoder for mounted URL. The mount path can contain parameter placeholders, i.e.
+ * <code>/mount/${foo}/path</code>. In that case the appropriate segment from the URL will be
+ * accessible as named parameter "foo" in the {@link PageParameters}. Similarly when the URL is
+ * constructed, the second segment will contain the value of the "foo" named page parameter.
+ * Optional parameters are denoted by using a # instead of $: <code>/mount/#{foo}/path/${bar}</code>
+ * has an optional {@code foo} parameter, a fixed {@code /path/} part and a required {@code bar}
+ * parameter. When in doubt, parameters are matched from left to right, where required parameters
+ * are matched before optional parameters, and optional parameters eager (from left to right).
+ * <p>
+ * Decodes and encodes the following URLs:
+ *
+ * <pre>
+ * Page Class - Render (BookmarkablePageRequestHandler for mounted pages)
+ * /mount/point
+ * (these will redirect to hybrid alternative if page is not stateless)
+ *
+ * IPage Instance - Render Hybrid (RenderPageRequestHandler for mounted pages)
+ * /mount/point?2
+ *
+ * IPage Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler for mounted pages)
+ * /mount/point?2-click-foo-bar-baz
+ * /mount/point?2-5.click.1-foo-bar-baz (1 is behavior index, 5 is render count)
+ * (these will redirect to hybrid if page is not stateless)
+ * </pre>
+ *
+ * @author Matej Knopp
+ */
+public class MountedMapper extends AbstractBookmarkableMapper
+{
+ private final IPageParametersEncoder pageParametersEncoder;
+
+ private static class MountPathSegment
+ {
+ private int segmentIndex;
+ private String fixedPart;
+ private int minParameters;
+ private int optionalParameters;
+
+ public MountPathSegment(int segmentIndex)
+ {
+ this.segmentIndex = segmentIndex;
+ }
+
+ public void setFixedPart(String fixedPart)
+ {
+ this.fixedPart = fixedPart;
+ }
+
+ public void addRequiredParameter()
+ {
+ minParameters++;
+ }
+
+ public void addOptionalParameter()
+ {
+ optionalParameters++;
+ }
+
+ public int getSegmentIndex()
+ {
+ return segmentIndex;
+ }
+
+ public String getFixedPart()
+ {
+ return fixedPart;
+ }
+
+ public int getMinParameters()
+ {
+ return minParameters;
+ }
+
+ public int getOptionalParameters()
+ {
+ return optionalParameters;
+ }
+
+ public int getMaxParameters()
+ {
+ return getOptionalParameters() + getMinParameters();
+ }
+
+ public int getFixedPartSize()
+ {
+ return getFixedPart() == null ? 0 : 1;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "(" + getSegmentIndex() + ") " + getMinParameters() + "-" + getMaxParameters() +
+ " " + (getFixedPart() == null ? "(end)" : getFixedPart());
+ }
+ }
+
+ private final List<MountPathSegment> pathSegments;
+ private final String[] mountSegments;
+
+ /** bookmarkable page class. */
+ private final ClassProvider<? extends IRequestablePage> pageClassProvider;
+
+ /**
+ * Construct.
+ *
+ * @param mountPath
+ * @param pageClass
+ */
+ public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass)
+ {
+ this(mountPath, pageClass, new PageParametersEncoder());
+ }
+
+ /**
+ * Construct.
+ *
+ * @param mountPath
+ * @param pageClassProvider
+ */
+ public MountedMapper(String mountPath,
+ ClassProvider<? extends IRequestablePage> pageClassProvider)
+ {
+ this(mountPath, pageClassProvider, new PageParametersEncoder());
+ }
+
+ /**
+ * Construct.
+ *
+ * @param mountPath
+ * @param pageClass
+ * @param pageParametersEncoder
+ */
+ public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass,
+ IPageParametersEncoder pageParametersEncoder)
+ {
+ this(mountPath, ClassProvider.of(pageClass), pageParametersEncoder);
+ }
+
+ /**
+ * Construct.
+ *
+ * @param mountPath
+ * @param pageClassProvider
+ * @param pageParametersEncoder
+ */
+ public MountedMapper(String mountPath,
+ ClassProvider<? extends IRequestablePage> pageClassProvider,
+ IPageParametersEncoder pageParametersEncoder)
+ {
+ Args.notEmpty(mountPath, "mountPath");
+ Args.notNull(pageClassProvider, "pageClassProvider");
+ Args.notNull(pageParametersEncoder, "pageParametersEncoder");
+
+ this.pageParametersEncoder = pageParametersEncoder;
+ this.pageClassProvider = pageClassProvider;
+ mountSegments = getMountSegments(mountPath);
+ pathSegments = getPathSegments(mountSegments);
+ }
+
+ private List<MountPathSegment> getPathSegments(String[] segments)
+ {
+ List<MountPathSegment> ret = new ArrayList<MountPathSegment>();
+ int segmentIndex = 0;
+ MountPathSegment curPathSegment = new MountPathSegment(segmentIndex);
+ ret.add(curPathSegment);
+ for (String curSegment : segments)
+ {
+ if (isFixedSegment(curSegment))
+ {
+ curPathSegment.setFixedPart(curSegment);
+ curPathSegment = new MountPathSegment(segmentIndex + 1);
+ ret.add(curPathSegment);
+ }
+ else if (getPlaceholder(curSegment) != null)
+ {
+ curPathSegment.addRequiredParameter();
+ }
+ else
+ {
+ curPathSegment.addOptionalParameter();
+ }
+ segmentIndex++;
+ }
+ return ret;
+ }
+
+ private boolean isFixedSegment(String segment)
+ {
+ return getOptionalPlaceholder(segment) == null && getPlaceholder(segment) == null;
+ }
+
+ /**
+ * @see AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+ */
+ @Override
+ protected UrlInfo parseRequest(Request request)
+ {
+ Url url = request.getUrl();
+
+ // when redirect to buffer/render is active and redirectFromHomePage returns true
+ // check mounted class against the home page class. if it matches let wicket redirect
+ // to the mounted URL
+ if (redirectFromHomePage() && checkHomePage(url))
+ {
+ return new UrlInfo(null, getContext().getHomePageClass(), newPageParameters());
+ }
+ // check if the URL starts with the proper segments
+ else if (urlStartsWith(url, mountSegments))
+ {
+ // try to extract page and component information from URL
+ PageComponentInfo info = getPageComponentInfo(url);
+ Class<? extends IRequestablePage> pageClass = getPageClass();
+ PageParameters pageParameters = extractPageParameters(request, url);
+
+ return new UrlInfo(info, pageClass, pageParameters);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /*
+ * extract the PageParameters from URL if there are any
+ */
+ private PageParameters extractPageParameters(Request request, Url url)
+ {
+ int[] matchedParameters = getMatchedSegmentSizes(url);
+ int total = 0;
+ for (int curMatchSize : matchedParameters)
+ total += curMatchSize;
+ PageParameters pageParameters = extractPageParameters(request, total, pageParametersEncoder);
+
+ int skippedParameters = 0;
+ for (int pathSegmentIndex = 0; pathSegmentIndex < pathSegments.size(); pathSegmentIndex++)
+ {
+ MountPathSegment curPathSegment = pathSegments.get(pathSegmentIndex);
+ int matchSize = matchedParameters[pathSegmentIndex] - curPathSegment.getFixedPartSize();
+ int optionalParameterMatch = matchSize - curPathSegment.getMinParameters();
+ for (int matchSegment = 0; matchSegment < matchSize; matchSegment++)
+ {
+ if (pageParameters == null)
+ {
+ pageParameters = new PageParameters();
+ }
+
+ int curSegmentIndex = matchSegment + curPathSegment.getSegmentIndex();
+ String curSegment = mountSegments[curSegmentIndex];
+ String placeholder = getPlaceholder(curSegment);
+ String optionalPlaceholder = getOptionalPlaceholder(curSegment);
+ // extract the parameter from URL
+ if (placeholder != null)
+ {
+ pageParameters.add(placeholder,
+ url.getSegments().get(curSegmentIndex - skippedParameters));
+ }
+ else if (optionalPlaceholder != null && optionalParameterMatch > 0)
+ {
+ pageParameters.add(optionalPlaceholder,
+ url.getSegments().get(curSegmentIndex - skippedParameters));
+ optionalParameterMatch--;
+ }
+ }
+ skippedParameters += curPathSegment.getMaxParameters() - matchSize;
+ }
+ return pageParameters;
+ }
+
+ @Override
+ protected boolean urlStartsWith(Url url, String... segments)
+ {
+ if (url == null)
+ {
+ return false;
+ }
+ else
+ {
+ return getMatchedSegmentSizes(url) != null;
+ }
+ }
+
+ private int[] getMatchedSegmentSizes(Url url)
+ {
+ int[] ret = new int[pathSegments.size()];
+ int segmentIndex = 0;
+ int pathSegmentIndex = 0;
+ for (MountPathSegment curPathSegment : pathSegments.subList(0, pathSegments.size() - 1))
+ {
+ boolean foundFixedPart = false;
+ segmentIndex += curPathSegment.getMinParameters();
+ int max = Math.min(curPathSegment.getOptionalParameters() + 1,
+ url.getSegments().size() - segmentIndex);
+
+ for (int count = max - 1; count >= 0; count--)
+ {
+ if (url.getSegments()
+ .get(segmentIndex + count)
+ .equals(curPathSegment.getFixedPart()))
+ {
+ foundFixedPart = true;
+ segmentIndex += count + 1;
+ ret[pathSegmentIndex] = count + curPathSegment.getMinParameters() + 1;
+ break;
+ }
+ }
+ if (!foundFixedPart)
+ return null;
+ pathSegmentIndex++;
+ }
+ MountPathSegment lastSegment = pathSegments.get(pathSegments.size() - 1);
+ segmentIndex += lastSegment.getMinParameters();
+ if (segmentIndex > url.getSegments().size())
+ return null;
+ ret[pathSegmentIndex] = Math.min(lastSegment.getMaxParameters(), url.getSegments().size() -
+ segmentIndex + lastSegment.getMinParameters());
+ return ret;
+ }
+
+ protected PageParameters newPageParameters()
+ {
+ return new PageParameters();
+ }
+
+ @Override
+ public Url mapHandler(IRequestHandler requestHandler)
+ {
+ Url url = super.mapHandler(requestHandler);
+
+ if (url == null && requestHandler instanceof ListenerInterfaceRequestHandler &&
+ getRecreateMountedPagesAfterExpiry())
+ {
+ ListenerInterfaceRequestHandler handler = (ListenerInterfaceRequestHandler)requestHandler;
+ IRequestablePage page = handler.getPage();
+ if (checkPageInstance(page))
+ {
+ String componentPath = handler.getComponentPath();
+ RequestListenerInterface listenerInterface = handler.getListenerInterface();
+
+ Integer renderCount = null;
+ if (listenerInterface.isIncludeRenderCount())
+ {
+ renderCount = page.getRenderCount();
+ }
+
+ PageInfo pageInfo = new PageInfo(page.getPageId());
+ ComponentInfo componentInfo = new ComponentInfo(renderCount,
+ requestListenerInterfaceToString(listenerInterface), componentPath,
+ handler.getBehaviorIndex());
+ PageComponentInfo pageComponentInfo = new PageComponentInfo(pageInfo, componentInfo);
+ PageParameters parameters = new PageParameters(page.getPageParameters());
+ UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
+ parameters.mergeWith(handler.getPageParameters()));
+ url = buildUrl(urlInfo);
+ }
+ }
+
+ return url;
+ }
+
+ boolean getRecreateMountedPagesAfterExpiry()
+ {
+ return Application.get().getPageSettings().getRecreateMountedPagesAfterExpiry();
+ }
+
+ /**
+ * @see AbstractBookmarkableMapper#buildUrl(AbstractBookmarkableMapper.UrlInfo)
+ */
+ @Override
+ protected Url buildUrl(UrlInfo info)
+ {
+ Url url = new Url();
+ for (String s : mountSegments)
+ {
+ url.getSegments().add(s);
+ }
+ encodePageComponentInfo(url, info.getPageComponentInfo());
+
+ PageParameters copy = new PageParameters(info.getPageParameters());
+
+ int dropped = 0;
+ for (int i = 0; i < mountSegments.length; ++i)
+ {
+ String placeholder = getPlaceholder(mountSegments[i]);
+ String optionalPlaceholder = getOptionalPlaceholder(mountSegments[i]);
+ if (placeholder != null)
+ {
+ url.getSegments().set(i - dropped, copy.get(placeholder).toString(""));
+ copy.remove(placeholder);
+ }
+ else if (optionalPlaceholder != null)
+ {
+ if (copy.getNamedKeys().contains(optionalPlaceholder))
+ {
+ url.getSegments().set(i - dropped, copy.get(optionalPlaceholder).toString(""));
+ copy.remove(optionalPlaceholder);
+ }
+ else
+ {
+ url.getSegments().remove(i - dropped);
+ dropped++;
+ }
+ }
+ }
+
+ return encodePageParameters(url, copy, pageParametersEncoder);
+ }
+
+ /**
+ * Check if the URL is for home page and the home page class match mounted class. If so,
+ * redirect to mounted URL.
+ *
+ * @param url
+ * @return request handler or <code>null</code>
+ */
+ private boolean checkHomePage(Url url)
+ {
+ if (url.getSegments().isEmpty() && url.getQueryParameters().isEmpty())
+ {
+ // this is home page
+ if (getPageClass().equals(getContext().getHomePageClass()) && redirectFromHomePage())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If this method returns <code>true</code> and application home page class is same as the class
+ * mounted with this encoder, request to home page will result in a redirect to the mounted
+ * path.
+ *
+ * @return whether this encode should respond to home page request when home page class is same
+ * as mounted class.
+ */
+ protected boolean redirectFromHomePage()
+ {
+ return true;
+ }
+
+ /**
+ * @see AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
+ */
+ @Override
+ protected boolean pageMustHaveBeenCreatedBookmarkable()
+ {
+ return false;
+ }
+
+ /**
+ * @see AbstractBookmarkableMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+ */
+ @Override
+ public int getCompatibilityScore(Request request)
+ {
+ if (urlStartsWith(request.getUrl(), mountSegments))
+ {
+ return mountSegments.length;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ /**
+ * @see AbstractBookmarkableMapper#checkPageClass(java.lang.Class)
+ */
+ @Override
+ protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
+ {
+ return pageClass.equals(this.getPageClass());
+ }
+
+ private Class<? extends IRequestablePage> getPageClass()
+ {
+ return pageClassProvider.get();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "MountedMapper [mountSegments=" + Strings.join("/", mountSegments) + "]";
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
new file mode 100644
index 0000000..949c244
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import java.lang.reflect.Modifier;
+
+import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.lang.PackageName;
+
+/**
+ * A request mapper that mounts all bookmarkable pages in a given package.
+ * <p>
+ * To mount this mapper onto a path use the {@link WebApplication#mountPackage(String, Class)}, ex:
+ *
+ * <pre>
+ * MyApp#init() {
+ *
+ * super.init();
+ * mountPackage("/my/path", MyPage.class);
+ * }
+ * </pre>
+ *
+ * will result in urls like {@code /my/path/MyPage}
+ * </p>
+ *
+ * <pre>
+ * Page Class - Render (BookmarkablePageRequestHandler)
+ * /MyPage
+ * (will redirect to hybrid alternative if page is not stateless)
+ *
+ * Page Instance - Render Hybrid (RenderPageRequestHandler for pages that were created using bookmarkable URLs)
+ * /MyPage?2
+ *
+ * Page Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler)
+ * /MyPage?2-click-foo-bar-baz
+ * /MyPage?2-click.1-foo-bar-baz (1 is behavior index)
+ * (these will redirect to hybrid if page is not stateless)
+ * </pre>
+ */
+public class PackageMapper extends AbstractBookmarkableMapper
+{
+ /**
+ * the name of the package for which all bookmarkable pages should be mounted
+ */
+ private final PackageName packageName;
+
+ /** the encoder used to encode/decode the page parameters */
+ private final IPageParametersEncoder pageParametersEncoder;
+
+ /**
+ * Construct.
+ *
+ * @param packageName
+ */
+ public PackageMapper(final PackageName packageName)
+ {
+ this(packageName, new PageParametersEncoder());
+ }
+
+ /**
+ * Construct.
+ *
+ * @param packageName
+ * @param pageParametersEncoder
+ */
+ public PackageMapper(final PackageName packageName,
+ final IPageParametersEncoder pageParametersEncoder)
+ {
+ Args.notNull(packageName, "packageName");
+ Args.notNull(pageParametersEncoder, "pageParametersEncoder");
+
+ this.packageName = packageName;
+ this.pageParametersEncoder = pageParametersEncoder;
+ }
+
+ /**
+ * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#buildUrl(org.apache.wicket.request.mapper.AbstractBookmarkableMapper.UrlInfo)
+ */
+ @Override
+ protected Url buildUrl(UrlInfo info)
+ {
+ Class<? extends IRequestablePage> pageClass = info.getPageClass();
+ PackageName pageClassPackageName = PackageName.forClass(pageClass);
+ if (pageClassPackageName.equals(packageName))
+ {
+ Url url = new Url();
+
+ String fullyQualifiedClassName = pageClass.getName();
+ String packageRelativeClassName = fullyQualifiedClassName;
+ int packageNameLength = packageName.getName().length();
+ if (packageNameLength > 0)
+ {
+ packageRelativeClassName = fullyQualifiedClassName.substring(packageNameLength + 1);
+ }
+ packageRelativeClassName = transformForUrl(packageRelativeClassName);
+ url.getSegments().add(packageRelativeClassName);
+ encodePageComponentInfo(url, info.getPageComponentInfo());
+ return encodePageParameters(url, info.getPageParameters(), pageParametersEncoder);
+ }
+
+ return null;
+ }
+
+ /**
+ * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+ */
+ @Override
+ protected UrlInfo parseRequest(Request request)
+ {
+ Url url = request.getUrl();
+ if (url.getSegments().size() >= 1)
+ {
+ // try to extract page and component information from URL
+ PageComponentInfo info = getPageComponentInfo(url);
+
+ // load the page class
+ String className = url.getSegments().get(0);
+
+ if (isValidClassName(className) == false)
+ {
+ return null;
+ }
+
+ className = transformFromUrl(className);
+ String fullyQualifiedClassName = packageName.getName() + '.' + className;
+ Class<? extends IRequestablePage> pageClass = getPageClass(fullyQualifiedClassName);
+
+ if (pageClass != null && Modifier.isAbstract(pageClass.getModifiers()) == false &&
+ IRequestablePage.class.isAssignableFrom(pageClass))
+ {
+ // extract the PageParameters from URL if there are any
+ PageParameters pageParameters = extractPageParameters(request, 1,
+ pageParametersEncoder);
+
+ return new UrlInfo(info, pageClass, pageParameters);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * filter out invalid class names for package mapper. getting trash for class names
+ * can e.g. happen when the home page is in the same package that is mounted by package mapper
+ * but the request was previously mapped by e.g. {@link HomePageMapper}. We then get some
+ * strange url like '/example/..' and wicket tries to look up class name '..'.
+ * <p/>
+ * @see <a href="https://issues.apache.org/jira/browse/WICKET-4303">WICKET-4303</a>
+ * <p/>
+ */
+ private boolean isValidClassName(String className)
+ {
+ // darn simple check - feel free to enhance this method to your needs
+ if (className == null)
+ {
+ return false;
+ }
+ // java class names never start with '.'
+ if (className.startsWith("."))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gives a chance to specializations of this mapper to transform the alias of the class name to
+ * the real class name
+ *
+ * @param classNameAlias
+ * the alias for the class name
+ * @return the real class name
+ */
+ protected String transformFromUrl(final String classNameAlias)
+ {
+ return classNameAlias;
+ }
+
+ /**
+ * Gives a chance to specializations of this mapper to transform the real class name to an alias
+ * which is prettier to represent in the Url
+ *
+ * @param className
+ * the real class name
+ * @return the class name alias
+ */
+ protected String transformForUrl(final String className)
+ {
+ return className;
+ }
+
+ /**
+ * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
+ */
+ @Override
+ protected boolean pageMustHaveBeenCreatedBookmarkable()
+ {
+ return true;
+ }
+
+ /**
+ * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+ */
+ @Override
+ public int getCompatibilityScore(Request request)
+ {
+ // always return 0 here so that the mounts have higher priority
+ return 0;
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
new file mode 100644
index 0000000..c133fc5
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler;
+import org.apache.wicket.core.request.handler.PageAndComponentProvider;
+import org.apache.wicket.core.request.handler.PageProvider;
+import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.info.ComponentInfo;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.info.PageInfo;
+
+/**
+ * Decodes and encodes the following URLs:
+ *
+ * <pre>
+ * Page Instance - Render (RenderPageRequestHandler)
+ * /wicket/page?2
+ *
+ *
+ * Page Instance - Listener (ListenerInterfaceRequestHandler)
+ * /wicket/page?2-click-foo-bar-baz
+ * /wicket/page?2-click.1-foo-bar-baz (1 is behavior index)
+ * </pre>
+ *
+ * @author Matej Knopp
+ */
+public class PageInstanceMapper extends AbstractComponentMapper
+{
+ /**
+ * Construct.
+ */
+ public PageInstanceMapper()
+ {
+ }
+
+ /**
+ * @see org.apache.wicket.request.IRequestMapper#mapRequest(org.apache.wicket.request.Request)
+ */
+ @Override
+ public IRequestHandler mapRequest(Request request)
+ {
+ Url url = request.getUrl();
+ if (matches(url))
+ {
+ PageComponentInfo info = getPageComponentInfo(url);
+ if (info != null && info.getPageInfo().getPageId() != null)
+ {
+ Integer renderCount = info.getComponentInfo() != null ? info.getComponentInfo()
+ .getRenderCount() : null;
+
+ if (info.getComponentInfo() == null)
+ {
+ PageProvider provider = new PageProvider(info.getPageInfo().getPageId(),
+ renderCount);
+ provider.setPageSource(getContext());
+ // render page
+ return new RenderPageRequestHandler(provider);
+ }
+ else
+ {
+ ComponentInfo componentInfo = info.getComponentInfo();
+
+ PageAndComponentProvider provider = new PageAndComponentProvider(
+ info.getPageInfo().getPageId(), renderCount,
+ componentInfo.getComponentPath());
+
+ provider.setPageSource(getContext());
+
+ // listener interface
+ RequestListenerInterface listenerInterface = requestListenerInterfaceFromString(componentInfo.getListenerInterface());
+
+ return new ListenerInterfaceRequestHandler(provider, listenerInterface,
+ componentInfo.getBehaviorId());
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @see org.apache.wicket.request.IRequestMapper#mapHandler(org.apache.wicket.request.IRequestHandler)
+ */
+ @Override
+ public Url mapHandler(IRequestHandler requestHandler)
+ {
+ PageComponentInfo info = null;
+
+ if (requestHandler instanceof RenderPageRequestHandler)
+ {
+ IRequestablePage page = ((RenderPageRequestHandler)requestHandler).getPage();
+
+ PageInfo i = new PageInfo(page.getPageId());
+ info = new PageComponentInfo(i, null);
+ }
+ else if (requestHandler instanceof ListenerInterfaceRequestHandler)
+ {
+ ListenerInterfaceRequestHandler handler = (ListenerInterfaceRequestHandler)requestHandler;
+ IRequestablePage page = handler.getPage();
+ String componentPath = handler.getComponentPath();
+ RequestListenerInterface listenerInterface = handler.getListenerInterface();
+
+ Integer renderCount = null;
+ if (listenerInterface.isIncludeRenderCount())
+ {
+ renderCount = page.getRenderCount();
+ }
+
+ PageInfo pageInfo = new PageInfo(page.getPageId());
+ ComponentInfo componentInfo = new ComponentInfo(renderCount,
+ requestListenerInterfaceToString(listenerInterface), componentPath,
+ handler.getBehaviorIndex());
+ info = new PageComponentInfo(pageInfo, componentInfo);
+ }
+
+ if (info != null)
+ {
+ Url url = new Url();
+ url.getSegments().add(getContext().getNamespace());
+ url.getSegments().add(getContext().getPageIdentifier());
+ encodePageComponentInfo(url, info);
+ return url;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * @see org.apache.wicket.request.IRequestMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+ */
+ @Override
+ public int getCompatibilityScore(final Request request)
+ {
+ int score = 0;
+ Url url = request.getUrl();
+ if (matches(url))
+ {
+ score = Integer.MAX_VALUE;
+ }
+ return score;
+ }
+
+ private boolean matches(final Url url)
+ {
+ return urlStartsWith(url, getContext().getNamespace(), getContext().getPageIdentifier());
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
new file mode 100644
index 0000000..d1d58cd
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import java.util.List;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.mapper.AbstractMapper;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
+import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
+import org.apache.wicket.request.resource.caching.ResourceUrl;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * A {@link IRequestMapper} to mount resources to a custom mount path
+ * <ul>
+ * <li>maps indexed parameters to path segments</li>
+ * <li>maps named parameters to query string arguments or placeholder path segments</li>
+ * </ul>
+ *
+ * <strong>sample structure of url</strong>
+ *
+ * <pre>
+ * /myresources/${category}/images/[indexed-param-0]/[indexed-param-1]?[named-param-1=value]&[named-param-2=value2]
+ * </pre>
+ *
+ * <h4>sample usage</h4>
+ *
+ * in your wicket application's init() method use a statement like this
+ * <p/>
+ *
+ * <pre>
+ * mountResource("/images", new ImagesResourceReference()));
+ * </pre>
+ *
+ * Note: Mounted this way the resource reference has application scope, i.e. it is shared between
+ * all users of the application. It is recommended to not keep any state in it.
+ *
+ * @see org.apache.wicket.protocol.http.WebApplication#mountResource(String,
+ * org.apache.wicket.request.resource.ResourceReference)
+ *
+ * @author Peter Ertl
+ */
+public class ResourceMapper extends AbstractMapper implements IRequestMapper
+{
+ // encode page parameters into url + decode page parameters from url
+ private final IPageParametersEncoder parametersEncoder;
+
+ // mount path (= segments) the resource is bound to
+ private final String[] mountSegments;
+
+ // resource that the mapper links to
+ private final ResourceReference resourceReference;
+
+ /**
+ * create a resource mapper for a resource
+ *
+ * @param path
+ * mount path for the resource
+ * @param resourceReference
+ * resource reference that should be linked to the mount path
+ *
+ * @see #ResourceMapper(String, org.apache.wicket.request.resource.ResourceReference,
+ * org.apache.wicket.request.mapper.parameter.IPageParametersEncoder)
+ */
+ public ResourceMapper(String path, ResourceReference resourceReference)
+ {
+ this(path, resourceReference, new PageParametersEncoder());
+ }
+
+ /**
+ * create a resource mapper for a resource
+ *
+ * @param path
+ * mount path for the resource
+ * @param resourceReference
+ * resource reference that should be linked to the mount path
+ * @param encoder
+ * encoder for url parameters
+ */
+ public ResourceMapper(String path, ResourceReference resourceReference,
+ IPageParametersEncoder encoder)
+ {
+ Args.notEmpty(path, "path");
+ Args.notNull(resourceReference, "resourceReference");
+ Args.notNull(encoder, "encoder");
+
+ this.resourceReference = resourceReference;
+ mountSegments = getMountSegments(path);
+ parametersEncoder = encoder;
+ }
+
+ @Override
+ public IRequestHandler mapRequest(final Request request)
+ {
+ final Url url = new Url(request.getUrl());
+
+ // now extract the page parameters from the request url
+ PageParameters parameters = extractPageParameters(request, mountSegments.length,
+ parametersEncoder);
+
+ // remove caching information from current request
+ removeCachingDecoration(url, parameters);
+
+ // check if url matches mount path
+ if (urlStartsWith(url, mountSegments) == false)
+ {
+ return null;
+ }
+
+ // check if there are placeholders in mount segments
+ for (int index = 0; index < mountSegments.length; ++index)
+ {
+ String placeholder = getPlaceholder(mountSegments[index]);
+
+ if (placeholder != null)
+ {
+ // extract the parameter from URL
+ if (parameters == null)
+ {
+ parameters = new PageParameters();
+ }
+ parameters.add(placeholder, url.getSegments().get(index));
+ }
+ }
+ return new ResourceReferenceRequestHandler(resourceReference, parameters);
+ }
+
+ @Override
+ public int getCompatibilityScore(Request request)
+ {
+ return 0; // pages always have priority over resources
+ }
+
+ @Override
+ public Url mapHandler(IRequestHandler requestHandler)
+ {
+ if ((requestHandler instanceof ResourceReferenceRequestHandler) == false)
+ {
+ return null;
+ }
+
+ ResourceReferenceRequestHandler handler = (ResourceReferenceRequestHandler)requestHandler;
+
+ // see if request handler addresses the resource reference we serve
+ if (resourceReference.equals(handler.getResourceReference()) == false)
+ {
+ return null;
+ }
+
+ Url url = new Url();
+
+ // add mount path segments
+ for (String segment : mountSegments)
+ {
+ url.getSegments().add(segment);
+ }
+
+ // replace placeholder parameters
+ PageParameters parameters = new PageParameters(handler.getPageParameters());
+
+ for (int index = 0; index < mountSegments.length; ++index)
+ {
+ String placeholder = getPlaceholder(mountSegments[index]);
+
+ if (placeholder != null)
+ {
+ url.getSegments().set(index, parameters.get(placeholder).toString(""));
+ parameters.remove(placeholder);
+ }
+ }
+
+ // add caching information
+ addCachingDecoration(url, parameters);
+
+ // create url
+ return encodePageParameters(url, parameters, parametersEncoder);
+ }
+
+ protected IResourceCachingStrategy getCachingStrategy()
+ {
+ return Application.get().getResourceSettings().getCachingStrategy();
+ }
+
+ protected void addCachingDecoration(Url url, PageParameters parameters)
+ {
+ final List<String> segments = url.getSegments();
+ final int lastSegmentAt = segments.size() - 1;
+ final String filename = segments.get(lastSegmentAt);
+
+ if (Strings.isEmpty(filename) == false)
+ {
+ // TODO is calling getResource() a potential performance bottleneck?
+ final IResource resource = resourceReference.getResource();
+
+ if (resource instanceof IStaticCacheableResource)
+ {
+ final IStaticCacheableResource cacheable = (IStaticCacheableResource)resource;
+ final ResourceUrl cacheUrl = new ResourceUrl(filename, parameters);
+
+ getCachingStrategy().decorateUrl(cacheUrl, cacheable);
+
+ if (Strings.isEmpty(cacheUrl.getFileName()))
+ {
+ throw new IllegalStateException("caching strategy returned empty name for " +
+ resource);
+ }
+ segments.set(lastSegmentAt, cacheUrl.getFileName());
+ }
+ }
+ }
+
+ protected void removeCachingDecoration(Url url, PageParameters parameters)
+ {
+ final List<String> segments = url.getSegments();
+
+ if (segments.isEmpty() == false)
+ {
+ // get filename (the last segment)
+ final int lastSegmentAt = segments.size() - 1;
+ String filename = segments.get(lastSegmentAt);
+
+ // ignore requests with empty filename
+ if (Strings.isEmpty(filename))
+ {
+ return;
+ }
+
+ // create resource url from filename and query parameters
+ final ResourceUrl resourceUrl = new ResourceUrl(filename, parameters);
+
+ // remove caching information from request
+ getCachingStrategy().undecorateUrl(resourceUrl);
+
+ // check for broken caching strategy (this must never happen)
+ if (Strings.isEmpty(resourceUrl.getFileName()))
+ {
+ throw new IllegalStateException("caching strategy returned empty name for " +
+ resourceUrl);
+ }
+
+ segments.set(lastSegmentAt, resourceUrl.getFileName());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
new file mode 100644
index 0000000..ce69bb2
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.request.mapper.ParentPathReferenceRewriter;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
+import org.apache.wicket.util.IProvider;
+
+/**
+ * Generic {@link ResourceReference} encoder that encodes and decodes non-mounted
+ * {@link ResourceReference}s.
+ * <p>
+ * Decodes and encodes the following URLs:
+ *
+ * <pre>
+ * /wicket/resource/org.apache.wicket.ResourceScope/name
+ * /wicket/resource/org.apache.wicket.ResourceScope/name?en
+ * /wicket/resource/org.apache.wicket.ResourceScope/name?-style
+ * /wicket/resource/org.apache.wicket.ResourceScope/resource/name.xyz?en_EN-style
+ * </pre>
+ *
+ * @author igor.vaynberg
+ */
+public class ResourceReferenceMapper extends ParentPathReferenceRewriter
+{
+ /**
+ * Construct.
+ *
+ * @param pageParametersEncoder
+ * @param parentPathPartEscapeSequence
+ * @param cachingStrategy
+ */
+ public ResourceReferenceMapper(IPageParametersEncoder pageParametersEncoder,
+ IProvider<String> parentPathPartEscapeSequence,
+ IProvider<IResourceCachingStrategy> cachingStrategy)
+ {
+ super(new BasicResourceReferenceMapper(pageParametersEncoder, cachingStrategy),
+ parentPathPartEscapeSequence);
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
new file mode 100644
index 0000000..a3af0e0
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.request.mapper;
+
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.request.component.IRequestablePage;
+
+/**
+ * Exception invoked when when stale link has been clicked. The page should then be rerendered with
+ * an explanatory error message.
+ *
+ * @author Matej Knopp
+ */
+public class StalePageException extends WicketRuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ private final transient IRequestablePage page;
+
+ /**
+ *
+ * Construct.
+ *
+ * @param page
+ */
+ public StalePageException(IRequestablePage page)
+ {
+ this.page = page;
+ }
+
+ /**
+ *
+ * @return page instance
+ */
+ public IRequestablePage getPage()
+ {
+ return page;
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
new file mode 100644
index 0000000..7fe43ca
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.util.crypt;
+
+import java.util.UUID;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Session;
+import org.apache.wicket.util.crypt.ICrypt;
+import org.apache.wicket.util.crypt.ICryptFactory;
+import org.apache.wicket.util.crypt.SunJceCrypt;
+
+/**
+ * Crypt factory that produces {@link SunJceCrypt} instances based on http session-specific
+ * encryption key. This allows each user to have their own encryption key, hardening against CSRF
+ * attacks.
+ *
+ * Note that the use of this crypt factory will result in an immediate creation of a http session
+ *
+ * @author igor.vaynberg
+ */
+public class KeyInSessionSunJceCryptFactory implements ICryptFactory
+{
+ /** metadata-key used to store crypto-key in session metadata */
+ private static MetaDataKey<String> KEY = new MetaDataKey<String>()
+ {
+ private static final long serialVersionUID = 1L;
+ };
+
+
+ @Override
+ public ICrypt newCrypt()
+ {
+ Session session = Session.get();
+ session.bind();
+
+ // retrieve or generate encryption key from session
+ String key = session.getMetaData(KEY);
+ if (key == null)
+ {
+ // generate new key
+ key = session.getId() + "." + UUID.randomUUID().toString();
+ session.setMetaData(KEY, key);
+ }
+
+ // build the crypt based on session key
+ ICrypt crypt = new SunJceCrypt();
+ crypt.setKey(key);
+ return crypt;
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java b/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
new file mode 100644
index 0000000..b29832b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.util.file;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+
+import org.apache.wicket.util.file.Folder;
+import org.apache.wicket.util.file.Path;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.core.util.resource.UrlResourceStream;
+import org.apache.wicket.util.string.StringList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Maintain a list of paths which might either be ordinary folders of the filesystem or relative
+ * paths to the web application's servlet context.
+ *
+ * @author Johan Compagner
+ */
+public final class WebApplicationPath extends Path
+{
+ private final static Logger log = LoggerFactory.getLogger(WebApplicationPath.class);
+
+ private static final String WEB_INF = "WEB-INF/";
+
+ /** The list of urls in the path */
+ private final List<String> webappPaths = new ArrayList<String>();
+
+ /** The web apps servlet context */
+ private final ServletContext servletContext;
+
+ /**
+ * Constructor
+ *
+ * @param servletContext
+ * The webapplication context where the resources must be loaded from
+ */
+ public WebApplicationPath(final ServletContext servletContext)
+ {
+ this.servletContext = servletContext;
+
+ // adding root so servlet context resources are always checked
+ webappPaths.add("/");
+ }
+
+ /**
+ * @param path
+ * add a path that is lookup through the servlet context
+ */
+ @Override
+ public void add(String path)
+ {
+ final Folder folder = new Folder(path);
+ if (folder.exists())
+ {
+ log.debug("Added path '{}' as a folder.", path);
+ super.add(folder);
+ }
+ else
+ {
+ if (!path.startsWith("/"))
+ {
+ path = "/" + path;
+ }
+ if (!path.endsWith("/"))
+ {
+ path += "/";
+ }
+ log.debug("Added path '{}' as a web path.", path);
+ webappPaths.add(path);
+ }
+ }
+
+ /**
+ *
+ * @see org.apache.wicket.util.file.IResourceFinder#find(Class, String)
+ */
+ @Override
+ public IResourceStream find(final Class<?> clazz, final String pathname)
+ {
+ if (pathname == null)
+ {
+ return null;
+ }
+
+ IResourceStream resourceStream = super.find(clazz, pathname);
+
+ if (resourceStream == null && pathname.startsWith(WEB_INF) == false)
+ {
+ for (String path : webappPaths)
+ {
+ try
+ {
+ final URL url = servletContext.getResource(path + pathname);
+ if (url != null)
+ {
+ resourceStream = new UrlResourceStream(url);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ // ignore, file couldn't be found
+ }
+ }
+ }
+
+ return resourceStream;
+ }
+
+ public List<String> getWebappPaths()
+ {
+ return webappPaths;
+ }
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString()
+ {
+ return "[folders = " + StringList.valueOf(getFolders()) + ", webapppaths: " +
+ StringList.valueOf(webappPaths) + "]";
+ }
+}
http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java b/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
new file mode 100644
index 0000000..9cc9829
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
@@ -0,0 +1,734 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.wicket.core.util.io;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.ObjectStreamField;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.WicketRuntimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Utility class that analyzes objects for non-serializable nodes. Construct, then call
+ * {@link #check(Object)} with the object you want to check. When a non-serializable object is
+ * found, a {@link WicketNotSerializableException} is thrown with a message that shows the trace up
+ * to the not-serializable object. The exception is thrown for the first non-serializable instance
+ * it encounters, so multiple problems will not be shown.
+ * <p>
+ * As this class depends heavily on JDK's serialization internals using introspection, analyzing may
+ * not be possible, for instance when the runtime environment does not have sufficient rights to set
+ * fields accessible that would otherwise be hidden. You should call
+ * {@link SerializableChecker#isAvailable()} to see whether this class can operate properly. If it
+ * doesn't, you should fall back to e.g. re-throwing/ printing the {@link NotSerializableException}
+ * you probably got before using this class.
+ * </p>
+ *
+ * @author eelcohillenius
+ * @author Al Maw
+ */
+public final class SerializableChecker extends ObjectOutputStream
+{
+
+ /** log. */
+ private static final Logger log = LoggerFactory.getLogger(SerializableChecker.class);
+
+ /**
+ * Exception that is thrown when a non-serializable object was found.
+ */
+ public static final class WicketNotSerializableException extends WicketRuntimeException
+ {
+ private static final long serialVersionUID = 1L;
+
+ WicketNotSerializableException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Does absolutely nothing.
+ */
+ private static class NoopOutputStream extends OutputStream
+ {
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public void flush()
+ {
+ }
+
+ @Override
+ public void write(byte[] b)
+ {
+ }
+
+ @Override
+ public void write(byte[] b, int i, int l)
+ {
+ }
+
+ @Override
+ public void write(int b)
+ {
+ }
+ }
+
+ private static abstract class ObjectOutputAdaptor implements ObjectOutput
+ {
+
+ @Override
+ public void close() throws IOException
+ {
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException
+ {
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException
+ {
+ }
+
+ @Override
+ public void write(int b) throws IOException
+ {
+ }
+
+ @Override
+ public void writeBoolean(boolean v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeByte(int v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeBytes(String s) throws IOException
+ {
+ }
+
+ @Override
+ public void writeChar(int v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeChars(String s) throws IOException
+ {
+ }
+
+ @Override
+ public void writeDouble(double v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeFloat(float v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeInt(int v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeLong(long v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeShort(int v) throws IOException
+ {
+ }
+
+ @Override
+ public void writeUTF(String str) throws IOException
+ {
+ }
+ }
+
+ /** Holds information about the field and the resulting object being traced. */
+ private static final class TraceSlot
+ {
+ private final String fieldDescription;
+
+ private final Object object;
+
+ TraceSlot(Object object, String fieldDescription)
+ {
+ super();
+ this.object = object;
+ this.fieldDescription = fieldDescription;
+ }
+
+ @Override
+ public String toString()
+ {
+ return object.getClass() + " - " + fieldDescription;
+ }
+ }
+
+ private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream();
+
+ /** Whether we can execute the tests. If false, check will just return. */
+ private static boolean available = true;
+
+ // this hack - accessing the serialization API through introspection - is
+ // the only way to use Java serialization for our purposes without writing
+ // the whole thing from scratch (and even then, it would be limited). This
+ // way of working is of course fragile for internal API changes, but as we
+ // do an extra check on availability and we report when we can't use this
+ // introspection fu, we'll find out soon enough and clients on this class
+ // can fall back on Java's default exception for serialization errors (which
+ // sucks and is the main reason for this attempt).
+ private static Method LOOKUP_METHOD;
+
+ private static Method GET_CLASS_DATA_LAYOUT_METHOD;
+
+ private static Method GET_NUM_OBJ_FIELDS_METHOD;
+
+ private static Method GET_OBJ_FIELD_VALUES_METHOD;
+
+ private static Method GET_FIELD_METHOD;
+
+ private static Method HAS_WRITE_REPLACE_METHOD_METHOD;
+
+ private static Method INVOKE_WRITE_REPLACE_METHOD;
+
+ static
+ {
+ try
+ {
+ LOOKUP_METHOD = ObjectStreamClass.class.getDeclaredMethod("lookup", new Class[] {
+ Class.class, Boolean.TYPE });
+ LOOKUP_METHOD.setAccessible(true);
+
+ GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+ "getClassDataLayout", (Class[])null);
+ GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true);
+
+ GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+ "getNumObjFields", (Class[])null);
+ GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true);
+
+ GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+ "getObjFieldValues", new Class[] { Object.class, Object[].class });
+ GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true);
+
+ GET_FIELD_METHOD = ObjectStreamField.class.getDeclaredMethod("getField", (Class[])null);
+ GET_FIELD_METHOD.setAccessible(true);
+
+ HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+ "hasWriteReplaceMethod", (Class[])null);
+ HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true);
+
+ INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+ "invokeWriteReplace", new Class[] { Object.class });
+ INVOKE_WRITE_REPLACE_METHOD.setAccessible(true);
+ }
+ catch (Exception e)
+ {
+ log.warn("SerializableChecker not available", e);
+ available = false;
+ }
+ }
+
+ /**
+ * Gets whether we can execute the tests. If false, calling {@link #check(Object)} will just
+ * return and you are advised to rely on the {@link NotSerializableException}. Clients are
+ * advised to call this method prior to calling the check method.
+ *
+ * @return whether security settings and underlying API etc allow for accessing the
+ * serialization API using introspection
+ */
+ public static boolean isAvailable()
+ {
+ return available;
+ }
+
+ /** object stack that with the trace path. */
+ private final LinkedList<TraceSlot> traceStack = new LinkedList<TraceSlot>();
+
+ /** set for checking circular references. */
+ private final Map<Object, Object> checked = new IdentityHashMap<Object, Object>();
+
+ /** string stack with current names pushed. */
+ private final LinkedList<String> nameStack = new LinkedList<String>();
+
+ /** root object being analyzed. */
+ private Object root;
+
+ /** set of classes that had no writeObject methods at lookup (to avoid repeated checking) */
+ private final Set<Class<?>> writeObjectMethodMissing = new HashSet<Class<?>>();
+
+ /** current simple field name. */
+ private String simpleName = "";
+
+ /** current full field description. */
+ private String fieldDescription;
+
+ /** Exception that should be set as the cause when throwing a new exception. */
+ private final NotSerializableException exception;
+
+ private final Stack<Object> stack = new Stack<Object>();
+
+ /**
+ * Construct.
+ *
+ * @param exception
+ * exception that should be set as the cause when throwing a new exception
+ *
+ * @throws IOException
+ */
+ public SerializableChecker(NotSerializableException exception) throws IOException
+ {
+ this.exception = exception;
+ }
+
+ /**
+ * @see java.io.ObjectOutputStream#reset()
+ */
+ @Override
+ public void reset() throws IOException
+ {
+ root = null;
+ checked.clear();
+ fieldDescription = null;
+ simpleName = null;
+ traceStack.clear();
+ nameStack.clear();
+ writeObjectMethodMissing.clear();
+ }
+
+ private void check(Object obj)
+ {
+ if (obj == null)
+ {
+ return;
+ }
+
+ try
+ {
+ if (stack.contains(obj))
+ {
+ return;
+ }
+ }
+ catch (RuntimeException e)
+ {
+ log.warn("Wasn't possible to check the object " + obj.getClass() +
+ " possible due an problematic implementation of equals method");
+ /*
+ * Can't check if this obj were in stack, giving up because we don't want to throw an
+ * invaluable exception to user. The main goal of this checker is to find non
+ * serializable data
+ */
+ return;
+ }
+
+ stack.push(obj);
+ try
+ {
+ internalCheck(obj);
+ }
+ finally
+ {
+ stack.pop();
+ }
+ }
+
+ private void internalCheck(Object obj)
+ {
+ if (obj == null)
+ {
+ return;
+ }
+
+ Class<?> cls = obj.getClass();
+ nameStack.add(simpleName);
+ traceStack.add(new TraceSlot(obj, fieldDescription));
+
+ if (!(obj instanceof Serializable) && (!Proxy.isProxyClass(cls)))
+ {
+ throw new WicketNotSerializableException(
+ toPrettyPrintedStack(obj.getClass().getName()), exception);
+ }
+
+ ObjectStreamClass desc;
+ for (;;)
+ {
+ try
+ {
+ desc = (ObjectStreamClass)LOOKUP_METHOD.invoke(null, cls, Boolean.TRUE);
+ Class<?> repCl;
+ if (!(Boolean)HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc, (Object[])null) ||
+ (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, obj)) == null ||
+ (repCl = obj.getClass()) == cls)
+ {
+ break;
+ }
+ cls = repCl;
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new RuntimeException(e);
+ }
+ catch (InvocationTargetException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ if (cls.isPrimitive())
+ {
+ // skip
+ }
+ else if (cls.isArray())
+ {
+ checked.put(obj, null);
+ Class<?> ccl = cls.getComponentType();
+ if (!(ccl.isPrimitive()))
+ {
+ Object[] objs = (Object[])obj;
+ for (int i = 0; i < objs.length; i++)
+ {
+ String arrayPos = "[" + i + "]";
+ simpleName = arrayPos;
+ fieldDescription += arrayPos;
+ check(objs[i]);
+ }
+ }
+ }
+ else if (obj instanceof Externalizable && (!Proxy.isProxyClass(cls)))
+ {
+ Externalizable extObj = (Externalizable)obj;
+ try
+ {
+ extObj.writeExternal(new ObjectOutputAdaptor()
+ {
+ private int count = 0;
+
+ @Override
+ public void writeObject(Object streamObj) throws IOException
+ {
+ // Check for circular reference.
+ if (checked.containsKey(streamObj))
+ {
+ return;
+ }
+
+ checked.put(streamObj, null);
+ String arrayPos = "[write:" + count++ + "]";
+ simpleName = arrayPos;
+ fieldDescription += arrayPos;
+
+ check(streamObj);
+ }
+ });
+ }
+ catch (Exception e)
+ {
+ if (e instanceof WicketNotSerializableException)
+ {
+ throw (WicketNotSerializableException)e;
+ }
+ log.warn("error delegating to Externalizable : " + e.getMessage() + ", path: " +
+ currentPath());
+ }
+ }
+ else
+ {
+ Method writeObjectMethod = null;
+ if (writeObjectMethodMissing.contains(cls) == false)
+ {
+ try
+ {
+ writeObjectMethod = cls.getDeclaredMethod("writeObject",
+ new Class[] { java.io.ObjectOutputStream.class });
+ }
+ catch (SecurityException e)
+ {
+ // we can't access / set accessible to true
+ writeObjectMethodMissing.add(cls);
+ }
+ catch (NoSuchMethodException e)
+ {
+ // cls doesn't have that method
+ writeObjectMethodMissing.add(cls);
+ }
+ }
+
+ final Object original = obj;
+ if (writeObjectMethod != null)
+ {
+ class InterceptingObjectOutputStream extends ObjectOutputStream
+ {
+ private int counter;
+
+ InterceptingObjectOutputStream() throws IOException
+ {
+ super(DUMMY_OUTPUT_STREAM);
+ enableReplaceObject(true);
+ }
+
+ @Override
+ protected Object replaceObject(Object streamObj) throws IOException
+ {
+ if (streamObj == original)
+ {
+ return streamObj;
+ }
+
+ counter++;
+ // Check for circular reference.
+ if (checked.containsKey(streamObj))
+ {
+ return null;
+ }
+
+ checked.put(streamObj, null);
+ String arrayPos = "[write:" + counter + "]";
+ simpleName = arrayPos;
+ fieldDescription += arrayPos;
+ check(streamObj);
+ return streamObj;
+ }
+ }
+ try
+ {
+ InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream();
+ ioos.writeObject(obj);
+ }
+ catch (Exception e)
+ {
+ if (e instanceof WicketNotSerializableException)
+ {
+ throw (WicketNotSerializableException)e;
+ }
+ log.warn("error delegating to writeObject : " + e.getMessage() + ", path: " +
+ currentPath());
+ }
+ }
+ else
+ {
+ Object[] slots;
+ try
+ {
+ slots = (Object[])GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc, (Object[])null);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ for (Object slot : slots)
+ {
+ ObjectStreamClass slotDesc;
+ try
+ {
+ Field descField = slot.getClass().getDeclaredField("desc");
+ descField.setAccessible(true);
+ slotDesc = (ObjectStreamClass)descField.get(slot);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ checked.put(obj, null);
+ checkFields(obj, slotDesc);
+ }
+ }
+ }
+
+ traceStack.removeLast();
+ nameStack.removeLast();
+ }
+
+ private void checkFields(Object obj, ObjectStreamClass desc)
+ {
+ int numFields;
+ try
+ {
+ numFields = (Integer)GET_NUM_OBJ_FIELDS_METHOD.invoke(desc, (Object[])null);
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new RuntimeException(e);
+ }
+ catch (InvocationTargetException e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ if (numFields > 0)
+ {
+ int numPrimFields;
+ ObjectStreamField[] fields = desc.getFields();
+ Object[] objVals = new Object[numFields];
+ numPrimFields = fields.length - objVals.length;
+ try
+ {
+ GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, obj, objVals);
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new RuntimeException(e);
+ }
+ catch (InvocationTargetException e)
+ {
+ throw new RuntimeException(e);
+ }
+ for (int i = 0; i < objVals.length; i++)
+ {
+ if (objVals[i] instanceof String || objVals[i] instanceof Number ||
+ objVals[i] instanceof Date || objVals[i] instanceof Boolean ||
+ objVals[i] instanceof Class)
+ {
+ // filter out common cases
+ continue;
+ }
+
+ // Check for circular reference.
+ if (checked.containsKey(objVals[i]))
+ {
+ continue;
+ }
+
+ ObjectStreamField fieldDesc = fields[numPrimFields + i];
+ Field field;
+ try
+ {
+ field = (Field)GET_FIELD_METHOD.invoke(fieldDesc, (Object[])null);
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new RuntimeException(e);
+ }
+ catch (InvocationTargetException e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ field.getName();
+ simpleName = field.getName();
+ fieldDescription = field.toString();
+ check(objVals[i]);
+ }
+ }
+ }
+
+ /**
+ * @return name from root to current node concatenated with slashes
+ */
+ private StringBuilder currentPath()
+ {
+ StringBuilder b = new StringBuilder();
+ for (Iterator<String> it = nameStack.iterator(); it.hasNext();)
+ {
+ b.append(it.next());
+ if (it.hasNext())
+ {
+ b.append('/');
+ }
+ }
+ return b;
+ }
+
+ /**
+ * Dump with indentation.
+ *
+ * @param type
+ * the type that couldn't be serialized
+ * @return A very pretty dump
+ */
+ private final String toPrettyPrintedStack(String type)
+ {
+ StringBuilder result = new StringBuilder();
+ StringBuilder spaces = new StringBuilder();
+ result.append("Unable to serialize class: ");
+ result.append(type);
+ result.append("\nField hierarchy is:");
+ for (Iterator<TraceSlot> i = traceStack.listIterator(); i.hasNext();)
+ {
+ spaces.append(" ");
+ TraceSlot slot = i.next();
+ result.append("\n").append(spaces).append(slot.fieldDescription);
+ result.append(" [class=").append(slot.object.getClass().getName());
+ if (slot.object instanceof Component)
+ {
+ Component component = (Component)slot.object;
+ result.append(", path=").append(component.getPath());
+ }
+ result.append("]");
+ }
+ result.append(" <----- field that is not serializable");
+ return result.toString();
+ }
+
+ /**
+ * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object)
+ */
+ @Override
+ protected final void writeObjectOverride(Object obj) throws IOException
+ {
+ if (!available)
+ {
+ return;
+ }
+ root = obj;
+ if (fieldDescription == null)
+ {
+ fieldDescription = (root instanceof Component) ? ((Component)root).getPath() : "";
+ }
+
+ check(root);
+ }
+}
\ No newline at end of file