60

Provided I have a java.net.URL object, pointing to let's say

http://example.com/myItems or http://example.com/myItems/

Is there some helper somewhere to append some relative URL to this? For instance append ./myItemId or myItemId to get : http://example.com/myItems/myItemId

thSoft
  • 21,755
  • 5
  • 88
  • 103
fulmicoton
  • 15,502
  • 9
  • 54
  • 74
  • possible duplicate of [Building an absolute URL from a relative URL in Java servlet](http://stackoverflow.com/questions/1389184/building-an-absolute-url-from-a-relative-url-in-java-servlet) – Don Roby Sep 21 '11 at 10:15
  • It seems there is no simple solution. Either URL or URI class doesn't support this. Apache HttpClient also doesn't have support for this. – Sri Jul 29 '17 at 02:43

21 Answers21

37

URL has a constructor that takes a base URL and a String spec.

Alternatively, java.net.URI adheres more closely to the standards, and has a resolve method to do the same thing. Create a URI from your URL using URL.toURI.

Behrang
  • 46,888
  • 25
  • 118
  • 160
Andrew Duffy
  • 6,800
  • 2
  • 23
  • 17
  • 2
    For benefit of others: The first URL constructor method didn't work quite the way that I expected it to (rtfm, eh?). Only the non-path portion of the baseURL is used. – Jasper Blues Jan 07 '16 at 05:31
  • 28
    "URL has a constructor that takes a base URL and a String spec." - Not helpful. URL url1 = new URL("http://petstore.swagger.wordnik.com/api/api-docs"); URL url2 = new URL(url1, "/pet"); System.out.println(url2.toString()) gives you: http://petstore.swagger.wordnik.com/pet, not http://petstore.swagger.wordnik.com/api/api-docs/pet – user2585038 Oct 14 '13 at 17:25
  • 9
    @user2585038: The second parameter must not start with a slash. The slash indicates an "absolute" link, thus the path of url1 is discarded. – harsel Jun 21 '16 at 08:40
  • 2
    URI resolve actually only works by mistake here. see @Christoph Henkelmann's answer from below – Ittai Oct 12 '16 at 20:17
  • 2
    As @user2585038 mentioned this answer does not take into account section 5.2 (6a) from [RFC2396](https://www.ietf.org/rfc/rfc2396.txt) which states: "All but the last segment of the base URI's path component is copied to the buffer. In other words, any characters after the last (right-most) slash character, if any, are excluded." This IMO makes URI resolving unusable in the majority of cases, including that of the OP. – steinybot Aug 23 '17 at 22:48
  • 1
    `URI.create(base).resolve(rest).toURL()` is the simplest way to get this done, without caring about duplicated `/` in between – jmm Jan 08 '21 at 17:57
  • `URI.resolve` has some very nasty edge cases `new URI("http://localhost:80").resolve("foo")` => `"http://localhost:80foo"` `new URI("http://localhost:80").resolve("//foo")` => `"http://foo"` `new URI("http://localhost:80").resolve(".//foo")`=> `"http://foo"` – iain Dec 02 '21 at 11:52
36

This one does not need any extra libs or code and gives the desired result:

//import java.net.URL;
URL url1 = new URL("http://petstore.swagger.wordnik.com/api/api-docs?foo=1&bar=baz");
URL url2 = new URL(url1.getProtocol(), url1.getHost(), url1.getPort(), url1.getPath() + "/pet" + "?" + url1.getQuery(), null);
System.out.println(url1);
System.out.println(url2);

This prints:

http://petstore.swagger.wordnik.com/api/api-docs?foo=1&bar=baz
http://petstore.swagger.wordnik.com/api/api-docs/pet?foo=1&bar=baz

The accepted answer only works if there is no path after the host (IMHO the accepted answer is wrong)

granadaCoder
  • 26,328
  • 10
  • 113
  • 146
Christoph Henkelmann
  • 1,437
  • 1
  • 12
  • 17
15

You can just use the URI class for this:

import java.net.URI;
import org.apache.http.client.utils.URIBuilder;

URI uri = URI.create("http://example.com/basepath/");
URI uri2 = uri.resolve("./relative");
// => http://example.com/basepath/relative

Note the trailing slash on the base path and the base-relative format of the segment that's being appended. You can also use the URIBuilder class from Apache HTTP client:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.3</version>
</dependency>

...

import java.net.URI;
import org.apache.http.client.utils.URIBuilder;

URI uri = URI.create("http://example.com/basepath");
URI uri2 = appendPath(uri, "relative");
// => http://example.com/basepath/relative

public URI appendPath(URI uri, String path) {
    URIBuilder builder = new URIBuilder(uri);
    builder.setPath(URI.create(builder.getPath() + "/").resolve("./" + path).getPath());
    return builder.build();
}
Scott Babcock
  • 549
  • 4
  • 13
  • 5
    The result of the first bit of code is ***not*** `http://example.com/basepath/relative`, but `http://example.com/relative`, unfortunately making the first part of this answer incorrect. – Luciano Oct 14 '19 at 08:14
  • Actually, the 1st part of the answer is correct; it works if the ./ is not provided as well if there are more than one / in the first URI. E.G. http://www.x.com:300/z/y/f.txt//////// ~~~ http://www.x.com:300/z/y/f.txt/zzccvr.tyt – Ironluca Mar 05 '20 at 10:36
11

I cannot believe how nasty URI.resolve() really is its full of nasty edge cases.

new URI("http://localhost:80").resolve("foo") => "http://localhost:80foo"
new URI("http://localhost:80").resolve("//foo") => "http://foo"
new URI("http://localhost:80").resolve(".//foo") => "http://foo"

The tidiest solution I have seen that handles these edge cases in an predictable way is:

URI addPath(URI uri, String path) {
    String newPath;
    if (path.startsWith("/")) newPath = path.replaceAll("//+", "/");
    else if (uri.getPath().endsWith("/")) newPath = uri.getPath() + path.replaceAll("//+", "/");
    else newPath = uri.getPath() + "/" + path.replaceAll("//+", "/");

    return uri.resolve(newPath).normalize();
}

Results:

jshell> addPath(new URI("http://localhost"), "sub/path")
$3 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost/"), "sub/path")
$4 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost/"), "/sub/path")
$5 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost/random-path"), "/sub/path")
$6 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost/random-path"), "./sub/path")
$7 ==> http://localhost/random-path/sub/path

jshell> addPath(new URI("http://localhost/random-path"), "../sub/path")
$8 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost"), "../sub/path")
$9 ==> http://localhost/../sub/path

jshell> addPath(new URI("http://localhost/"), "//sub/path")
$10 ==> http://localhost/sub/path

jshell> addPath(new URI("http://localhost/"), "//sub/./path")
$11 ==> http://localhost/sub/path
iain
  • 10,798
  • 3
  • 37
  • 41
  • 1
    The result from `URI.resolve` with the double-slash in front is correct, this is called a `protocol-relative URL` and it used to be more common, but is now discouraged. See [wiki](https://en.wikipedia.org/wiki/URL#prurl). Your `addPath` is correct if the 2nd parameter is always a path, but `URI.resolve` is handling the more general case where it may be a full URL. – Kyle Pittman Mar 16 '23 at 19:49
8

Here is a helper function I've written to add to the url path:

public static URL concatenate(URL baseUrl, String extraPath) throws URISyntaxException, 
                                                                    MalformedURLException {
    URI uri = baseUrl.toURI();
    String newPath = uri.getPath() + '/' + extraPath;
    URI newUri = uri.resolve(newPath);
    return newUri.toURL();
}
Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205
  • 7
    If `baseUrl` ends with a `/` or `extraPath` starts with a `/` you will have repeated `/` in the final URL. You should add a `.normalize()` before the last `toURL()`. – Simão Martins May 19 '17 at 14:42
  • This does not work for a local file URL which should start with file:// – Sundar Rajan Jul 02 '18 at 06:38
  • if `baseUrl.getPath()` is `"/"` this will modify the host!!! `URI("http://localhost:80").resolve("//foo")` => `"http://foo"` I show how to address these below. – iain Dec 02 '21 at 11:56
7

You can use URIBuilder and the method URI#normalize to avoid duplicate / in the URI:

URIBuilder uriBuilder = new URIBuilder("http://example.com/test");
URI uri = uriBuilder.setPath(uriBuilder.getPath() + "/path/to/add")
          .build()
          .normalize();
// expected : http://example.com/test/path/to/add
herau
  • 1,466
  • 2
  • 18
  • 36
4

I've searched far and wide for an answer to this question. The only implementation I can find is in the Android SDK: Uri.Builder. I've extracted it for my own purposes.

private String appendSegmentToPath(String path, String segment) {
  if (path == null || path.isEmpty()) {
    return "/" + segment;
  }

  if (path.charAt(path.length() - 1) == '/') {
    return path + segment;
  }

  return path + "/" + segment;
}

This is where I found the source.

In conjunction with Apache URIBuilder, this is how I'm using it: builder.setPath(appendSegmentToPath(builder.getPath(), segment));

twhitbeck
  • 113
  • 8
  • This can be used same way Using UriBuilder of jersey client/ http://docs.oracle.com/javaee/7/api/javax/ws/rs/core/UriBuilder.html – Optional Apr 18 '17 at 06:34
3

UPDATED

I believe this is the shortest solution:

URL url1 = new URL("http://domain.com/contextpath");
String relativePath = "/additional/relative/path";
URL concatenatedUrl = new URL(url1.toExternalForm() + relativePath);
Galya
  • 6,294
  • 6
  • 27
  • 45
3

Concatenate a relative path to a URI:

java.net.URI uri = URI.create("https://stackoverflow.com/questions")
java.net.URI res = uri.resolve(uri.getPath + "/some/path")

res will contain https://stackoverflow.com/questions/some/path

Martin Tapp
  • 3,106
  • 3
  • 32
  • 39
2

On Android you can use android.net.Uri. The following allows to create an Uri.Builder from an existing URL as String and then append:

Uri.parse(baseUrl) // Create Uri from String
    .buildUpon()   // Creates a "Builder"
    .appendEncodedPath("path/to/add")
    .appendQueryParameter("at_ref", "123") // To add ?at_ref=123
    .fragment("anker") // To add #anker
    .build()

Note that appendEncodedPath doesn't expect a leading / and only contains a check if the "baseUrl" ends with one, otherwise one is added before the path.

According to the docs, this supports

  • Absolute hierarchical URI reference following the pattern

    • <scheme>://<authority><absolute path>?<query>#<fragment>
  • Relative URI with pattern

    • <relative or absolute path>?<query>#<fragment>
    • //<authority><absolute path>?<query>#<fragment>
  • Opaque URI with pattern

    • <scheme>:<opaque part>#<fragment>
sunadorer
  • 3,855
  • 34
  • 42
2

A pragmatical solution without any external libs is given below.

(Comment: After reading through all the answers given so far, I am really not happy with the solutions provided - especially as this question is eight years old. No solution does deal properly with queries, fragments and so on.)

Extension method on URL

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

class URLHelper {
        public static URL appendRelativePathToURL(URL base, String relPath) {
            /*
              foo://example.com:8042/over/there?name=ferret#nose
              \_/   \______________/\_________/ \_________/ \__/
               |           |            |            |        |
            scheme     authority       path        query   fragment
               |   _____________________|__
              / \ /                        \
              urn:example:animal:ferret:nose

            see https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
            */
            try {

                URI baseUri = base.toURI();

                // cut initial slash of relative path
                String relPathToAdd = relPath.startsWith("/") ? relPath.substring(1) : relPath;

                // cut trailing slash of present path
                String path = baseUri.getPath();
                String pathWithoutTrailingSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;

                return new URI(baseUri.getScheme(),
                        baseUri.getAuthority(),
                        pathWithoutTrailingSlash + "/" + relPathToAdd,
                        baseUri.getQuery(),
                        baseUri.getFragment()).toURL();
            } catch (URISyntaxException e) {
                throw new MalformedURLRuntimeException("Error parsing URI.", e);
            } catch (MalformedURLException e) {
                throw new MalformedURLRuntimeException("Malformed URL.", e);
            }
        }

        public static class MalformedURLRuntimeException extends RuntimeException {
            public MalformedURLRuntimeException(String msg, Throwable cause) {
                super("Malformed URL: " + msg, cause);
            }
        }
    }

Testing

    private void demo() {

        try {
            URL coolURL = new URL("http://fun.de/path/a/b/c?query&another=3#asdf");
            URL notSoCoolURL = new URL("http://fun.de/path/a/b/c/?query&another=3#asdf");
            System.out.println(URLHelper.appendRelativePathToURL(coolURL, "d"));
            System.out.println(URLHelper.appendRelativePathToURL(coolURL, "/d"));
            System.out.println(URLHelper.appendRelativePathToURL(notSoCoolURL, "d"));
            System.out.println(URLHelper.appendRelativePathToURL(notSoCoolURL, "/d"));

        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
Martin Senne
  • 5,939
  • 6
  • 30
  • 47
1

Some examples using the Apache URIBuilder http://hc.apache.org/httpcomponents-client-4.3.x/httpclient/apidocs/org/apache/http/client/utils/URIBuilder.html:

Ex1:

String url = "http://example.com/test";
URIBuilder builder = new URIBuilder(url);
builder.setPath((builder.getPath() + "/example").replaceAll("//+", "/"));
System.out.println("Result 1 -> " + builder.toString());

Result 1 -> http://example.com/test/example

Ex2:

String url = "http://example.com/test";
URIBuilder builder = new URIBuilder(url);
builder.setPath((builder.getPath() + "///example").replaceAll("//+", "/"));
System.out.println("Result 2 -> " + builder.toString());

Result 2 -> http://example.com/test/example

Marcelo C.
  • 3,822
  • 2
  • 22
  • 11
1

My solution based on twhitbeck answer:

import java.net.URI;
import java.net.URISyntaxException;

public class URIBuilder extends org.apache.http.client.utils.URIBuilder {
    public URIBuilder() {
    }

    public URIBuilder(String string) throws URISyntaxException {
        super(string);
    }

    public URIBuilder(URI uri) {
        super(uri);
    }

    public org.apache.http.client.utils.URIBuilder addPath(String subPath) {
        if (subPath == null || subPath.isEmpty() || "/".equals(subPath)) {
            return this;
        }
        return setPath(appendSegmentToPath(getPath(), subPath));
    }

    private String appendSegmentToPath(String path, String segment) {
        if (path == null || path.isEmpty()) {
            path = "/";
        }

        if (path.charAt(path.length() - 1) == '/' || segment.startsWith("/")) {
            return path + segment;
        }

        return path + "/" + segment;
    }
}

Test:

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class URIBuilderTest {

    @Test
    public void testAddPath() throws Exception {
        String url = "http://example.com/test";
        String expected = "http://example.com/test/example";

        URIBuilder builder = new URIBuilder(url);
        builder.addPath("/example");
        assertEquals(expected, builder.toString());

        builder = new URIBuilder(url);
        builder.addPath("example");
        assertEquals(expected, builder.toString());

        builder.addPath("");
        builder.addPath(null);
        assertEquals(expected, builder.toString());

        url = "http://example.com";
        expected = "http://example.com/example";

        builder = new URIBuilder(url);
        builder.addPath("/");
        assertEquals(url, builder.toString());
        builder.addPath("/example");
        assertEquals(expected, builder.toString());
    }
}

Gist: https://gist.github.com/enginer/230e2dc2f1d213a825d5

Enginer
  • 3,048
  • 1
  • 26
  • 22
1

I had some difficulty with the encoding of URI's. Appending was not working for me because it was of a content:// type and it was not liking the "/". This solution assumes no query, nor fragment(we are working with paths after all):

Kotlin code:

  val newUri = Uri.parse(myUri.toString() + Uri.encode("/$relPath"))
johnml1135
  • 4,519
  • 3
  • 23
  • 18
1

Support for appending paths was added to URIBuilder in Apache HttpClient 5.1 with the appendPath method:

import org.apache.hc.core5.net.URIBuilder;
..
URI uri = new URIBuilder("https://stackoverflow.com/questions")
  .appendPath("7498030")
  .appendPath("append-relative-url")
  .build();

// https://stackoverflow.com/questions/7498030/append-relative-url

Maven dependency:

<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
  <version>5.1</version>
</dependency>
lars
  • 640
  • 4
  • 10
0

For android make sure you use .appendPath() from android.net.Uri

Roman Gherta
  • 821
  • 2
  • 15
  • 27
0
public String joinUrls(String baseUrl, String extraPath) {
        try {
            URI uri = URI.create(baseUrl+"/");//added additional slash in case there is no slash at either sides
            URI newUri = uri.resolve(extraPath);
            return newUri.toURL().toString();
        } catch (IllegalArgumentException | MalformedURLException e) {
            //exception
        }
}
0

An handmade uri segments joiner

public static void main(String[] args) {
    System.out.println(concatURISegments(
            "http://abc/",
            "/dfg/",
            "/lmn",
            "opq"
    ));
}

public static String concatURISegments(String... segmentArray) {
    if (segmentArray.length == 0) {
        return "";
    } else if (segmentArray.length == 1) {
        return segmentArray[0];
    }

    List<String> segmentList = new ArrayList<>();
    for (String s : segmentArray) {
        if (s != null && s.length() > 0) {
            segmentList.add(s);
        }
    }

    if (segmentList.size() == 0) {
        return "";
    } else if (segmentList.size() == 1) {
        return segmentList.get(0);
    }

    StringBuilder sb = new StringBuilder();
    sb.append(segmentList.get(0));
    String prevS;
    String currS;
    boolean prevB;
    boolean currB;
    for (int i = 1; i < segmentList.size(); i++) {
        prevS = segmentList.get(i - 1);
        currS = segmentList.get(i);
        prevB = prevS.endsWith("/");
        currB = currS.startsWith("/");
        if (!prevB && !currB) {
            sb.append("/").append(currS);
        } else if (prevB && currB) {
            sb.append(currS.substring(1));
        } else {
            sb.append(currS);
        }
    }
    return sb.toString();
}
Codemix
  • 56
  • 3
0

This takes only one line, normalize() is your friend here, and always add an extra / inbetween the concatenation

When baseUrl ends with / the normalize() would remove the extra ones. If it doesn't end with / then we've covered it by adding one deliberately.

String unknownBaseUrl = "https://example.com/apples/";
String result = URI.create(unknownBaseUrl + "/" + "1209").normalize().toString();
System.out.println(result);

output: https://example.com/apples/1209

Sample with many extra / will be normalized to a sane path as per the RFC 2396

String unknownBaseUrl = "https://example.com/apples/";
String result = URI.create(unknownBaseUrl + "/" + "/1209").normalize().toString();
System.out.println(result);

output: https://example.com/apples/1209

Onewildgamer
  • 101
  • 1
  • 2
  • 11
0

To get around all the edge cases the best would be to combine two standard classes - UriBuilder from apache.httpclient and java.nio.file.Paths:

String rootUrl = "http://host:80/root/url";
String relativePath = "relative/path";

URIBuilder builder = new URIBuilder(rootUrl);
String combinedPath = Paths.get(builder.getPath(), relativePath).toString();
builder.setPath(combinedPath);

URL targetUrl = builder.build().toURL();

It results in: http://host:80/root/url/relative/path

This works with any number of leading and trailing / and also when / are absent.

0

With Spring framework:

import org.springframework.web.util.DefaultUriBuilderFactory;
...
var url = "http://localhost"
var uriBuilderFactory = new DefaultUriBuilderFactory(url)
var target_uri = uriBuilderFactory.builder().pathSegment("path1").pathSegment("path2").build().toString()
//
// result: http://localhost/path1/path2
Shtefan
  • 742
  • 12
  • 14