diff --git a/core/src/main/java/org/kohsuke/stapler/AncestorImpl.java b/core/src/main/java/org/kohsuke/stapler/AncestorImpl.java index 03f153cc4..6450c20ee 100644 --- a/core/src/main/java/org/kohsuke/stapler/AncestorImpl.java +++ b/core/src/main/java/org/kohsuke/stapler/AncestorImpl.java @@ -43,13 +43,17 @@ class AncestorImpl implements Ancestor { private final boolean endsWithSlash; AncestorImpl(RequestImpl req, Object object) { - this.owner = req.ancestors; + this(req.ancestors, object, req.tokens, req.getContextPath()); + } + + AncestorImpl(List owner, Object object, TokenList tokens, String contextPath) { + this.owner = owner; listIndex = owner.size(); this.object = object; - this.tokens = req.tokens.rawTokens; - this.index = req.tokens.idx; - this.endsWithSlash = req.tokens.endsWithSlash; - this.contextPath = req.getContextPath(); + this.tokens = tokens.rawTokens; + this.index = tokens.idx; + this.endsWithSlash = tokens.endsWithSlash; + this.contextPath = contextPath; } void addToOwner() { diff --git a/core/src/main/java/org/kohsuke/stapler/Dispatcher.java b/core/src/main/java/org/kohsuke/stapler/Dispatcher.java index 6b5fd1f63..1eba19d05 100644 --- a/core/src/main/java/org/kohsuke/stapler/Dispatcher.java +++ b/core/src/main/java/org/kohsuke/stapler/Dispatcher.java @@ -23,6 +23,7 @@ package org.kohsuke.stapler; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jakarta.servlet.ServletException; import java.io.IOException; @@ -53,6 +54,17 @@ public abstract class Dispatcher { public abstract boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException; + /** + * If this is a purely navigational dispatcher, indicates the next ancestor object. + * @param node starting point + * @param tokens the path to parse through + * @return the next node, if any + */ + @CheckForNull + public Object next(Object node, TokenList tokens) { + return null; + } + /** * Diagnostic string that explains this dispatch rule. */ diff --git a/core/src/main/java/org/kohsuke/stapler/MetaClass.java b/core/src/main/java/org/kohsuke/stapler/MetaClass.java index 92e2903bf..3d8718852 100644 --- a/core/src/main/java/org/kohsuke/stapler/MetaClass.java +++ b/core/src/main/java/org/kohsuke/stapler/MetaClass.java @@ -379,6 +379,19 @@ public boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) } } + @Override + protected Object doNext(Object node, TokenList tokens) { + if (isAccepted) { + String token = tokens.next(); + return ff.invoke(req, rsp, node, token); // TODO will need a new method in Function too + } else { + String token = tokens.next(); + tokens.prev(); + return null; + // TODO is FilteredGetterTriggerListener relevant in this context? + } + } + @Override public String toString() { if (isAccepted) { diff --git a/core/src/main/java/org/kohsuke/stapler/NameBasedDispatcher.java b/core/src/main/java/org/kohsuke/stapler/NameBasedDispatcher.java index 050920d83..076012f8f 100644 --- a/core/src/main/java/org/kohsuke/stapler/NameBasedDispatcher.java +++ b/core/src/main/java/org/kohsuke/stapler/NameBasedDispatcher.java @@ -23,6 +23,7 @@ package org.kohsuke.stapler; +import edu.umd.cs.findbugs.annotations.CheckForNull; import jakarta.servlet.ServletException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -67,4 +68,25 @@ public final boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) protected abstract boolean doDispatch(RequestImpl req, ResponseImpl rsp, Object node) throws IOException, ServletException, IllegalAccessException, InvocationTargetException; + + @Override + public final Object next(Object node, TokenList tokens) { + if (!tokens.hasMore() || !tokens.peek().equals(name)) { + return null; + } + if (tokens.countRemainingTokens() <= argCount) { + return null; + } + tokens.next(); + var n = doNext(node, tokens); + if (n == null) { + tokens.prev(); + } + return n; + } + + @CheckForNull + protected Object doNext(Object node, TokenList tokens) { + return null; + } } diff --git a/core/src/main/java/org/kohsuke/stapler/Stapler.java b/core/src/main/java/org/kohsuke/stapler/Stapler.java index 234f382c9..838a31cb7 100644 --- a/core/src/main/java/org/kohsuke/stapler/Stapler.java +++ b/core/src/main/java/org/kohsuke/stapler/Stapler.java @@ -256,6 +256,22 @@ protected void service(HttpServletRequest req, HttpServletResponse rsp) throws S } } + /** + * Calculates what ancestors in path would have been used for a given URL. + * Like actual request processing, this will call methods like {@code getXXX} + * expecting them to have no side effects. + * Will not load views, run {@code doXXX} methods, or otherwise invoke terminal operations. + * @param servletPath like {@link StaplerRequest2#getServletPath} + * @return like {@link StaplerRequest2#getAncestors} + */ + public List parseAncestors(String servletPath) { + var tokens = new TokenList(servletPath); + var ancestors = new ArrayList(); + // TODO follow same logic as tryInvoke incl. addToOwner, StaplerProxy, StaplerOverridable, StaplerFallback, + // but call Dispatcher.next after finding MetaClass + return Collections.unmodifiableList(ancestors); + } + /** * Tomcat and GlassFish returns a fresh {@link InputStream} every time * {@link URLConnection#getInputStream()} is invoked in their {@code org.apache.naming.resources.DirContextURLConnection}. diff --git a/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java b/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java index 91b4eb996..e7ab68a72 100644 --- a/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java +++ b/core/src/main/java/org/kohsuke/stapler/StaplerRequest2.java @@ -189,6 +189,21 @@ public interface StaplerRequest2 extends HttpServletRequest { */ Ancestor findAncestor(Object o); + /** + * Finds ancestor objects which can be inferred from the referer header, if any. + * This is useful when the current request is triggered by JavaScript from a page + * and needs to look up certain types of objects in that page. + */ + default List getAncestorsFromReferer() { + var referer = getReferer(); + var rootPath = getRootPath(); + if (referer != null && referer.startsWith(rootPath)) { + return getStapler().parseAncestors(referer.substring(rootPath.length())); + } else { + return List.of(); + } + } + /** * Short for {@code getParameter(name)!=null} */