How do you resolve a relative Uri?
Asked Answered
D

2

10

Given an absolute Uri and a relative Uri or relative path, how do you get the absolute Uri pointing to the relative location?

For example, suppose we have the Uri for file:///android_asset/dir, pointing to a location in our assets. Further suppose that elsewhere, we have a relative path of /foo. The absolute Uri for that relative path should be file:///android_asset/foo. Is there something on Uri, or elsewhere in the Android SDK, that I am missing that give me that result Uri?

Uri.withAppendedPath() is not the answer, as all it seems to do is handle trailing directory separators:

Uri abs=Uri.parse("file:///android_asset/");
Uri rel=Uri.withAppendedPath(abs, "/foo");
Assert.assertEquals("file:///android_asset/foo", rel.toString());
  // actually returns file:///android_asset//foo

Uri abs2=Uri.parse("file:///android_asset/dir");
Uri rel2=Uri.withAppendedPath(abs2, "/foo");
Assert.assertEquals("file:///android_asset/foo", rel2.toString());
  // actually returns file:///android_asset/dir//foo

Uri.Builder, via buildUpon() on Uri, is not an improvement:

Uri rel3=abs.buildUpon().appendPath("/foo").build();
Assert.assertEquals("file:///android_asset/foo", rel3.toString());
  // actually returns file:///android_asset/%2Ffoo

Uri rel4=abs.buildUpon().appendEncodedPath("/foo").build();
Assert.assertEquals("file:///android_asset/foo", rel4.toString());
// actually returns file:///android_asset//foo

In a pinch I can try using java.net.URL and its URL(URL context, String spec) constructor, or just roll some code for it, but I was hoping to stay in the realm of Android Uri values if possible, just for any quirks differentiating URL and Uri.

Dripping answered 15/9, 2015 at 15:37 Comment(11)
no need to add slash for appendPath - it does it for you. I have my method getBaseUriBuilder to get Uri.Builder with schema+root. So all you need is to append path and query params...Weed
"So all you need is to append path" -- there are many possible relative path structures. /foo is just one. There is also foo, ./foo, ../foo, ../../foo/bar, and so on. Many will use part of the path of the absolute Uri, others (e.g., /foo) will not. I'm trying to handle all the situations, if possible.Dripping
see File construtors: File(File dir, String name) and/or File(String dirPath, String name) and File#getCanonicalPath()Canto
@pskink: Yes, but not every Uri is a File. For example, your approach will not work for http URLs (port, fragment, query parameters, etc.), file:///android_asset/ (since a relative Uri for it would need to retain the /android_asset/ part), etc. If you're saying "rip all the extraneous stuff off, use File to handle the relative-ness, handle android_asset yourself, and then re-assemble all the stuff you ripped off", while that's possible, that's what "just roll some code for it" meant. :-)Dripping
@Dripping yes, I wouldn't call it a silver bullet, but anything is better than nothing. ;-)Canto
Why don't you try Uri rel=Uri.withAppendedPath(abs, "foo");, then the return will be file:///android_asset/fooExcitability
@BNK: As I wrote in a previous comment, there are many possible relative path structures. /foo is just one. There is also foo, ./foo, ../foo, ../../foo/bar, and so on. Many will use part of the path of the absolute Uri, others (e.g., /foo) will not. I'm trying to handle all the situations, if possible.Dripping
@Dripping how about these lines URI absolute = new URI("file:////android_asset/"); URI relative = new URI("foo/bar"); System.out.println(absolute.resolve(relative)); // I/System.out﹕ file://android_asset/foo/bar relative = new URI("./foo/bar"); System.out.println(absolute.resolve(relative)); // I/System.out﹕ file://android_asset/foo/bar relative = new URI("../foo"); System.out.println(absolute.resolve(relative)); // I/System.out﹕ file://fooExcitability
@BNK: That last one should result in an error in an Android-aware Uri resolution algorithm.Dripping
Yes, it will be OK if the absolute looks like file:////android_asset/abc/xyz, am I right?Excitability
@BNK: Yes. Basically, android_asset in file:///android_asset is "magic" and has to be treated differently, as if it were the host of a URL. Regular file:/// values do not have that restriction. The net is that it appears that there isn't something in the SDK for handling this resolution, and I'll be cobbling something together using URI and/or Uri.Builder.Dripping
S
4

Android doesn't make this easy.

In my case, I had to take a base url that may or may not have an included path:

http://www.myurl.com/myapi/

...and append a REST API method path, like:

api/where/agencies-with-coverage.json

...to produce the entire url:

http://www.myurl.com/myapi/api/where/agencies-with-coverage.json

Here's how I did it (compiled from various methods within the app - there may be a simpler way of doing this):

String baseUrlString = "http://www.myurl.com/myapi/";
String pathString = "api/where/agencies-with-coverage.json";
Uri.Builder builder = new Uri.Builder();
builder.path(pathString);

Uri baseUrl = Uri.parse(baseUrlString);

// Copy partial path (if one exists) from the base URL
Uri.Builder path = new Uri.Builder();
path.encodedPath(baseUrl.getEncodedPath());

// Then, tack on the rest of the REST API method path
path.appendEncodedPath(builder.build().getPath());

// Finally, overwrite builder with the full URL
builder.scheme(baseUrl.getScheme());
builder.encodedAuthority(baseUrl.getEncodedAuthority());
builder.encodedPath(path.build().getEncodedPath());

// Final Uri
Uri finalUri = builder.build();

In my case, the Builder classes for the API client code assembled the path prior to combining it with the baseURL, so that explains the order of things above.

If I've pulled together the above code correctly, it should handle port numbers as well as spaces in the URL string.

I pulled this source code from the OneBusAway Android app, specifically the ObaContext class. Note that this code on Github also handles the additional case where the user typed in a baseUrl (String serverName = Application.get().getCustomApiUrl() in the above code) that should override the region base URL (mRegion.getObaBaseUrl()), and the user-entered URL may not have http:// in front of it.

The unit tests that pass for the above code on Github, including cases where port numbers and spaces are included in the baseUrl and path, and the leading/trailing / may or may not be included, are here on Github. Related issues on Github where I was banging my head on the wall to try and get this all to work - 72, 126, 132.

I haven't tried this with non-HTTP URIs, but I believe it may work more generally.

Soberminded answered 17/9, 2015 at 19:24 Comment(3)
And, fixed another typo.Soberminded
Technically, this doesn't answer the question. What you're describing is what I called "just roll some code for it" in the question. From this and the comments, I am inferring that there's no simple in-SDK solution that I missed. So, I'll grant you the bounty. Thanks for the help!Dripping
Yeah, this is as close as I could get using native Android classes. I agree that given the complexity it does fall into the "roll your own" category. Thanks for the bounty!Soberminded
C
1

There is an equivalent to urllib.parse.urljoin (Python) in Android URI.create(baseUrl).resolve(path).

import java.net.URI

URI.create("https://dmn92m25mtw4z.cloudfront.net/helpvids/f3_4/hls_480/480.m3u8")
    .resolve("0.ts")
// output:
// https://dmn92m25mtw4z.cloudfront.net/helpvids/f3_4/hls_480/0.ts

Sean Barbeau answer returns wrong URL, it's just appending the 0.ts to the url.

Carliecarlile answered 20/3, 2022 at 8:2 Comment(1)
Thanks, but note that I was referring to Uri (the Android-specific class), not URI (the java.net class). It is possible that they are interchangeable, but I would not assume that without more testing.Dripping

© 2022 - 2024 — McMap. All rights reserved.