Let a factory claim the URL being parsed, not only the calling class - #63
Let a factory claim the URL being parsed, not only the calling class#63paulrutter wants to merge 4 commits into
Conversation
…ports The plurl copy that wins the install in a JVM is the one that routes, and it may be older than the copy a factory brought with it -- another framework instance, or an application embedding its own. A factory registering with such a router has no way to know that a capability it depends on will never be consulted, so it registers and is then silently misrouted. That matters here: bundle: URLs can only be attributed to a framework by the UUID they carry, so without shouldHandle(String, String) they go to whichever factory registered first. Adds a plurlCapabilities operation reporting a Set<String>, and Plurl.capabilities() as the convenience form. An implementation predating the operation rejects it with an IOException, so absence of an answer is the answer and is reported as an empty set. Also tolerates a leading slash in the operation path, so the documented "plurl://op/<operation>" form works and is not mistaken for an older plurl. Pushed upstream as part of eclipse-osgi-technology/plurl#63. 54 tests pass there, including one covering this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Added a second commit: It comes from your point about released signatures staying in use in the same JVM. The copy that wins the install is the one that routes, and it may be older than the copy a factory brought with it. A factory then registers successfully and is silently misrouted, with no way to tell. For selection by spec that is the difference between working and quietly resolving to the wrong framework, so being able to detect it seems worth more than the API costs: Set<String> caps = Plurl.capabilities(); // plurl://op/plurlCapabilities
caps.contains(Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC);An implementation predating the operation rejects it with Two things worth flagging in that commit:
Rebased onto the branch after the On the Felix side this is used to warn at startup when the installed router can't route |
54d2a60 to
5b814bc
Compare
A protocol can be shared by several factories that are distinguishable only by
the URL itself: multiple instances of the same framework in one JVM all use
the same protocol, and the owner is an id in the URL host. Such a URL may also
be parsed by a caller that no factory recognises from the call stack, which
leaves nothing at all to select on, and the first factory added is chosen. It
then cannot resolve an id that belongs to another instance.
Adds an optional hook consulted in findFactory before the call stack is
walked:
default boolean shouldHandle(String protocol, String spec)
The spec is passed rather than a URL, because selection happens while the URL
is still being parsed -- its host and path are not populated yet, and
parseURL pins the chosen handler onto the URL immediately afterwards, so there
is no later point at which the URL could be inspected. Implementations are
therefore given only strings, and must not call back into URL handling.
Defaulting to false leaves every existing factory unaffected. The signature
uses only java.lang types, so the reflective path finds the method on
factories compiled against a different copy of this package, and factories
that predate it are tolerated rather than failing; that matters while released
signatures remain in use in the same JVM. The reflective lookup is cached,
because it sits on the URL parsing path and a missing method must not cost a
lookup and an exception per URL.
PlurlFactoryHolder delegates the new method the same way it already delegates
shouldHandle(Class); without that, findFactory only ever asks the holders,
which answer with the interface default.
The new test fails without the change (both URLs are served by the
first-added factory) and passes with it.
Signed-off-by: Paul Rütter <rutterpaul+github-personal@gmail.com>
Copies of different plurl versions can be in use in the same JVM, and the copy that wins the install is the one that routes. A factory registering with an older implementation than itself has no way to know that a capability it depends on will not be consulted, so it registers and is then silently misrouted. That matters for shouldHandle(String, String): a factory identifiable only by the URL, rather than by protocol or call stack, cannot be routed to at all without it. Being able to detect that and say so is much better than appearing to work. Adds a plurlCapabilities operation reporting a Set<String> of capability names, and Plurl.capabilities() as the convenience form. An implementation that predates the operation rejects it with an IOException, so absence of an answer is itself the answer and is reported as an empty set -- callers treat a missing capability as unsupported rather than as an error. Two incidental points: - plurlOperation now tolerates a leading slash in the path, so the "plurl://op/<operation>" form the javadoc documents works as well as the URL(protocol, host, file) form the convenience methods use. Without this the documented form is rejected as an unknown operation, which for a capability query would be indistinguishable from an older plurl. - getContent() on the operation connection widens from Consumer<Object> to Object, since a query returns a value rather than an operation. Callers go through URLConnection.getContent() and already cast, so this is source compatible; the lambdas now carry an explicit target type. Signed-off-by: Paul Rütter <rutterpaul+github-personal@gmail.com>
The editorconfig check added in the recent main merge runs super-linter with
VALIDATE_ALL_CODEBASE false, so it lints only the files a pull request changes
-- but it reports every violation in those files, not only the changed lines.
Plurl.java, PlurlFactory.java and PlurlImpl.java carry 18 trailing-whitespace
lines that predate this branch (javadoc continuation lines, plus one "try { "),
so the check fails on this PR even though it added none of them.
Whitespace only, in the three files already touched, kept in its own commit so
it can be dropped or taken separately.
Signed-off-by: Paul Rütter <rutterpaul+github-personal@gmail.com>
5b814bc to
3acc0c5
Compare
| * @param spec the spec the URL is being parsed from, which may be relative | ||
| * @return true if this factory should handle the URL | ||
| */ | ||
| default boolean shouldHandle(String protocol, String spec) { |
There was a problem hiding this comment.
This method should be moved to org.eclipse.osgitech.plurl.PlurlStreamHandlerFactory because it is only called against URLStreamHandlerFactory registered factories.
Perhaps it should be called shouldHandleURL. I am thinking it should be made more specific in name and interface because we may need to do a similar enhancement to org.eclipse.osgitech.plurl.PlurlContentHandlerFactory and protocol and spec would not make sense there. There we would need something like:
default boolean shouldHandleContent(String mimetype) { return false; }Having different method names would make the reflection path more clear I think, if we decide we need this for content handlers.
There was a problem hiding this comment.
To be clear, I am not suggesting we jump on implementing this for content handlers. I would wait and see if such functionality is really needed before doing that.
There was a problem hiding this comment.
Thanks for your swift response, addressing your feedback in this commit.
Agreed and done: it is now shouldHandleURL(String protocol, String spec) on PlurlStreamHandlerFactory. The delegation moved with it, from PlurlFactoryHolder to URLStreamHandlerFactoryHolder, so the hook is only consulted on the stream path. Nothing added for content handlers, per your follow-up.
| import org.junit.Before; | ||
| import org.junit.Test; | ||
|
|
||
| /** |
There was a problem hiding this comment.
Can you also add a factory to the test method org.eclipse.osgitech.plurl.test.PlurlStreamHandlerFactoryTest.doTestURLContext(TestFactoryType, PlurlTestHandlers, PlurlTestHandlers) that takes over a protocol such that we can test that it is used over any of the other registered factories for the expected protocol it took over. That would also make sure the check by class stack path works even when there is a factory that took over a specific protocol for the other protocols.
I know these tests can be unwieldy, but they are necessary to test the different types of registrations: reflective, copy, shared classes, legacy etc.
There was a problem hiding this comment.
Thanks for your swift response, addressing your feedback in this commit.
Done, and it earned its keep immediately: it found a real gap. createURLStreamHandler is asked per protocol with no URL, and selected a factory by call stack only, so a protocol served solely by a factory that claims it by URL was declined before parseURL could ever select it. All five registration types failed on the first run. The protocol is now also claimed when a factory that selects on the URL serves it.
doTestURLContext gains a fourth protocol served by two factories: one claiming it by calling class, one taking it over by URL. It asserts the taken-over protocol resolves from both contexts — including the one whose class no factory for that protocol claims, which only selection by URL can do — and that the by-class factory was never asked for a handler. Selection by call stack is still asserted for the other protocols alongside it, which was your second point.
TestFactory gained takeOver(String...) and a count of handlers handed out, so a test can tell which factory a URL was actually routed to. The reflective and copy paths needed the hook on TestNotPlurlStreamHandlerFactory and on the test.copy interface to exercise them — that is precisely the mixed-version case you raised at the start, so the unwieldiness was worth it.
55 tests pass.
| * This is a convenience method for using the plurl protocol like this: | ||
| * | ||
| * <pre> | ||
| * ((Set<String>) ("plurl://op/plurlCapabilities").getContent()); |
There was a problem hiding this comment.
If we are using a Set<String> then we just have boolean type of on/off capability types. I'm fine with that, because I really hope we don't need to add many of these and I don't anticipate we would need/want more complex capabilities than simple on/off ones.
But with that in mind would it be more simple if the spec was plurl://op/getCapability/<capability-name>.
/**
* The plurl protocol operation to check a capability.
*/
public static final String PLURL_GET_CAPABILITY = "getCapability"; //$NON-NLS-1$
public static final String PLURL_CAPABILITY_SELECT_BY_SPEC = "selectFactoryBySpec"; //$NON-NLS-1$
Then used like this:
Boolean isSelectFactoryBySpec = (Boolean) new URL("plurl://op/getCapability/selectFactoryBySpec").getContent());This also gives us the freedom to specify the capability type (e.g. Boolean for on/off capabilities) in the javadoc for the capability constant and also gives us the freedom to use other types for future capabilities if we need to.
There was a problem hiding this comment.
Thanks for your swift response, addressing your feedback in this commit.
Switched to plurl://op/getCapability/<capability-name>. Agreed on the reasoning — typing per capability is worth more than enumeration, which nothing needed. Plurl.capabilities() is now Plurl.getCapability(String), returning the value or null when unsupported.
One property worth keeping in view: an unknown capability is rejected with IOException the same way an unknown operation is, so the absence of an answer still tells an older plurl apart from one that answers. That is what the Felix side relies on to warn rather than silently misroute. Tests cover the known capability, an unknown one, the protocol form with its leading slash, and an unknown operation.
| return Collections.emptySet(); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
We lost the javadoc for the add(PlurlStreamHandlerFactory factory). The doc is up above the doc for the new method capabilities method.
There was a problem hiding this comment.
Good catch, and my mistake: I anchored the insertion on the method signature, so the new method landed between add's javadoc and add itself. Restored, and the new method has its own.
Follows tjwatson's review of eclipse-osgi-technology/plurl#63. The selection hook moves from PlurlFactory to PlurlStreamHandlerFactory and is renamed shouldHandleURL: it is only consulted for URLStreamHandlerFactory registrations, and a content handler equivalent would need a different signature, so a distinct name keeps the reflective lookup unambiguous. The capabilities set becomes a per-capability query, plurl://op/getCapability/<name>, returning the capability value so its type can be specified per capability. An unknown capability is rejected the same way an unknown operation is, so absence of an answer still tells an older plurl apart. Also fixes a real gap the review's requested test exposed: the JVM asks createURLStreamHandler per protocol with no URL, and a factory was selected by call stack only, so a protocol served solely by a factory that claims it by URL was declined before parseURL could select it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Moved the selection hook from PlurlFactory to PlurlStreamHandlerFactory and renamed it shouldHandleURL. It is only consulted for URLStreamHandlerFactory registrations, and a content handler equivalent would need a different signature, so a distinct name keeps the reflective lookup unambiguous. The delegation moved with it, from PlurlFactoryHolder to URLStreamHandlerFactoryHolder. - Replaced the capabilities set with a per-capability query, plurl://op/getCapability/<name>, returning the capability value so the type can be specified per capability. An unknown capability is rejected the same way an unknown operation is, so absence of an answer still distinguishes an older plurl. Plurl.capabilities() becomes Plurl.getCapability(String). - Restored the javadoc for add(PlurlStreamHandlerFactory), which the previous commit had orphaned onto the new method. - Extended PlurlStreamHandlerFactoryTest.doTestURLContext with a factory that takes over a protocol, asserting it serves that protocol from both test contexts -- including the one whose class no factory for that protocol claims, which only selection by URL can do -- and that the factory claiming the same protocol by calling class was not used. Selection by call stack is still asserted for the other protocols alongside it. TestFactory gained a takeOver(String...) and a count of handlers handed out, so a test can tell which factory a URL was routed to. That test found a real gap: createURLStreamHandler is asked per protocol with no URL, and selected a factory by call stack only, so a protocol served solely by a factory that claims it by URL was declined before parseURL could select it. The protocol is now also claimed when a factory that selects on the URL serves it. Found by all five registration types; the reflective and copy paths needed the hook on the test factories to exercise them. 55 tests pass. Signed-off-by: Paul Rütter <rutterpaul+github-personal@gmail.com>
209c767 to
99cd4ba
Compare
Fixes #62.
A protocol can be shared by several factories that are distinguishable only by the URL itself, and there is currently no way to route on that. Multiple instances of the same framework in one JVM all use the same protocol, and the owner is an id in the URL host. Such a URL can also be parsed by a caller that no factory recognises from the call stack, which leaves nothing to select on at all — the first factory added is chosen, and it cannot resolve an id belonging to another instance. Background and the Apache Felix case are in #62.
Change
An optional hook consulted in
findFactorybefore the call stack is walked:PlurlFactoryHolderdelegates it the way it already delegatesshouldHandle(Class)— without that,findFactoryonly ever asks the holders, which answer with the interface default.Why a spec and not a
URL. Selection happens while the URL is still being parsed:parseURLruns before the host and path are populated, andsetHandlerpins the chosen handler onto the URL immediately afterwards. I implemented theURLform first and it is unreachable whereverjava.netis open — including this repo's own test build (--add-opens java.base/java.net) and the Felix framework bundle (Add-opens: java.base/java.net), because the handler swap then succeeds and plurl never regains control atopenConnection. The spec is the one thing that exists at the moment plurl has to choose. Implementations are therefore handed only strings, and must not call back into URL handling.Compatibility
Intended to be safe while released signatures stay in use in the same JVM:
defaultreturningfalse, so existing factories are unaffected and nothing needs recompiling. Java 8 baseline is unchanged (this compiles atrelease 8).java.langtypes in the signature, so the reflective path finds the method on a factory compiled against a different copy of this package.Two-argument, so it does not overload
shouldHandle(Class)andshouldHandle(null)stays unambiguous.Tests
PlurlURLSelectionTest: two factories claiming by host, one URL each, neither claiming anything by call stack. Both cases fail without the change (expected:<second> but was:<first>) and pass with it, including a URL rebuilt from its external form. 51/51 green.Not included
When no factory claims a URL, plurl selects the first factory added. Declining looks safer to me — a factory that cannot serve the URL returns a handler that then answers wrongly or fails confusingly — but that is a behaviour change for existing users, so I left it alone. Raised as a question in #62.