63

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

2

21 Answers 21

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.

8
  • 31
    "URL has a constructor that takes a base URL and a String spec." - Not helpful. URL url1 = new URL("petstore.swagger.wordnik.com/api/api-docs"); URL url2 = new URL(url1, "/pet"); System.out.println(url2.toString()) gives you: petstore.swagger.wordnik.com/pet, not petstore.swagger.wordnik.com/api/api-docs/pet Commented Oct 14, 2013 at 17:25
  • 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. Commented Jan 7, 2016 at 5:31
  • 11
    @user2585038: The second parameter must not start with a slash. The slash indicates an "absolute" link, thus the path of url1 is discarded.
    – harsel
    Commented Jun 21, 2016 at 8:40
  • 2
    URI resolve actually only works by mistake here. see @Christoph Henkelmann's answer from below
    – Ittai
    Commented Oct 12, 2016 at 20:17
  • 2
    As @user2585038 mentioned this answer does not take into account section 5.2 (6a) from RFC2396 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
    Commented Aug 23, 2017 at 22:48
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)

8
  • 1
    MalformedUrlException why don't they just throw unchecked exception :'(
    – gkiko
    Commented Aug 9, 2016 at 9:03
  • Yeah, that sucks... and it's inconsistent: parsing numbers throws unchecked exceptions ^^ Commented Aug 11, 2016 at 14:49
  • 2
    This solution doesn't handle trailing slashes. The question provides two examples.
    – rrhrg
    Commented Aug 2, 2018 at 17:08
  • 1
    url1.getFile().endsWith("/") ? url1.getFile() + "pet" : url1.getFile() + "/pet"? Commented Nov 7, 2018 at 15:01
  • 1
    @MartinSenne Well spotted, I adjusted the example. Commented May 27, 2020 at 14:57
19

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();
}
2
  • 6
    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
    Commented Oct 14, 2019 at 8: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. x.com:300/z/y/f.txt/////// ~~~ x.com:300/z/y/f.txt/zzccvr.tyt
    – Ironluca
    Commented Mar 5, 2020 at 10:36
16

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
1
  • 2
    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. 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. Commented Mar 16, 2023 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();
}
3
  • 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(). Commented May 19, 2017 at 14:42
  • This does not work for a local file URL which should start with file:// Commented Jul 2, 2018 at 6: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
    Commented Dec 2, 2021 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
1
  • Jakarta WS has a jakarta.ws.rs.core.UriBuilder which might already be available in your project which provides similar functionality. UriBuilder.fromUri(baseUri).path("path/to/add").build(); allows appending path segments to a URI. Commented May 30 at 9:23
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));

1
4

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

2
  • 1
    If you run this code, I think you'll find that the result is: Commented Mar 12, 2018 at 17:11
  • Corrected code to use uri.getPath instead, thanks! Commented Mar 13, 2018 at 18:02
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);
1
  • new URL("/additional/relative/path"); throws MalformedURLException Commented Jan 21, 2016 at 17:57
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();
        }
    }
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>
2

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.

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

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

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>
1

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

0

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

0

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"))
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();
}
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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.