3

Can someone explain what is the difference between the following two calls to ServletContext getRealPath() in Tomcat:

  • context.getRealPath("/") + "\\songModified.wav";
  • context.getRealPath("/" + "\\songModified.wav");

I have a very simple GET method on the server which reads a file on the server and copies the bytes into a new file in the location returned by the above call.

On the client side I have an audio tag that references an audio file on the server, calls this method that creates a new file and changes the reference of the audio tag to this new file. The thing is that in the javascript callback this new file is not immediately referenceable if I store the file to the path that is returned from the second case of the above getRealPath call. Basically it returns a 404. If I store it to the returned path of the first case of the call then it is immediately referenceable and the audio tag normaly references the new file.

Both of those calls to getRealPath() return exactly the same string:

C:\Users\Mihael\apache-tomcat-9.0.31\wtpwebapps\AudioSimulator\songModified.wav

I am passing this returned string to the FileOutputStream constructor further in the code.

Thing to note here is that this file does not exist at the moment of the getRealPath() call so I am confused why is it returning anything at all in the second case of the call.

I know this is not the recommended way of storing files so I am asking from a purely educational perspective. How can the second call to this method break my functionality if they both return exactly the same string to the rest of the code?

EDIT:

Here is a very simple Javascript and Java code for anyone who wants to test this.

Javascript:

<body>
<script>

function modifyRequest() {
    var xhttp = new XMLHttpRequest();

    xhttp.onload = function() {
      var audio = document.getElementById("player");
      var currentTime = audio.currentTime;
      audio.src = "http://localhost:8080/AudioSimulator/bluesModified.wav";
      audio.currentTime = currentTime;
      audio.play();
    };

    xhttp.open("GET", "http://localhost:8080/AudioSimulator/rest/Test/testPath");  
    xhttp.send();
}

</script>

<audio id="player" src="http://localhost:8080/AudioSimulator/blues.wav"
        controls>
            Your browser does not support the
            <code>audio</code> element.
    </audio>    

    <button onclick="modifyRequest()">Test</button>

</body>

Java:

    @Path("/Test")
public class Test {

    @Context
    ServletContext context;

    @GET
    @Path("/testPath")
    public Response testPath() {
        File fileIn = new File(context.getRealPath("/") + "\\blues.wav");
        File fileOut = new File(context.getRealPath("/" + "\\bluesModified.wav"));
        //if i write it like this it would work
        //File fileOut = new File(context.getRealPath("/") + "\\bluesModified.wav");

        FileInputStream fis = null;
        FileOutputStream fos = null;

        try {
            fis = new FileInputStream(fileIn);
            fos = new FileOutputStream(fileOut);
            byte[] inArray = new byte[(int) fileIn.length()];
            try {
                fis.read(inArray);
                fos.write(inArray);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return Response
                .ok()
                .entity("Success")
                .header("Access-Control-Allow-Origin", "null")
                .build();
    }

}
Michael Munta
  • 207
  • 2
  • 16
  • 1
    I know you are windows The way I would write it is context.getRealPath("/songModified.wav"); There is no need to use backslashes. – rickz Apr 15 '20 at 19:14
  • Or use context.getRealPath("/" + "/songModified.wav") – rickz Apr 15 '20 at 19:22
  • Maybe this helps https://stackoverflow.com/questions/12160639/what-does-servletcontext-getrealpath-mean-and-when-should-i-use-it – Marcinek Apr 15 '20 at 19:23
  • The point is my system works in the first case, but it does not in the second case(notice the different usage in parentheses). I don't get any errors in java code and the file is created normally in both cases, but for some reason I can not reference it from javascript callback in the second case. – Michael Munta Apr 15 '20 at 19:39
  • Did you look at Dev tools in browser to see what paths the browser is looking for? – rickz Apr 15 '20 at 20:03
  • Well the paths are fine since if I refresh the browser after calling the method once the file is already created and then it works. – Michael Munta Apr 15 '20 at 20:58
  • I agree to rickz to always use slashes for path separators on all modern operating systems (i think Windows supports this since NT4 in 1996). Whatever your problems are, that makes it easier ;-) – jazz64 Apr 17 '20 at 15:27
  • I have added example code for anyone who is willing to test this. – Michael Munta Apr 18 '20 at 10:44
  • I don't understand why you are using either of them actually. What's wrong with `getRealPath("/bluesModified.wav")`? Why all the complication? – user207421 Apr 18 '20 at 12:32
  • I noted that the question is educational. I am not using it anywhere I just want to know why would it work differently when the functions return the same thing. – Michael Munta Apr 18 '20 at 13:14

2 Answers2

2

I have taken the time to dive into Tomcat source to find the cause for this. It turns out that getRealPath, in addition to retrieving the system path for a given virtual path, also works a bit with the Tomcat cache.

NOTE:

I know that my file separator usage is not good, but Tomcat is smart enough to validate the above call to produce /bluesModified.wav. So even if I call it like @rickz mentioned in the comments, the result would be the same and therefore that was not the issue.

The issues I had with being unable to reference the file in the case of the following call

context.getRealPath("/" + "\\bluesModified.wav")

was the fact that in this case we are passing the file path to the method, while in the case that works we are passing in the directory path.

What happens is that the call to getRealPath() first checks the cache for the existence of the resource identified by the webapppath /bluesModified.wav. Since it does not exist at the moment of the call, Tomcat will create an instance of the EmptyResource class which is basically a wrapper around File class and represents a file that does not exist, and it will then store the reference to this file in its cache.

The issue here is that even though I create a file that will have the correct virtual path Tomcat will still have that empty resource representing a non existent file in its cache. In other words, if I reference the file from the client side like so

http://localhost:8080/AudioSimulator/bluesModified.wav

Tomcat will return the cached resource that represents the empty file, which actually means a 404 to the client even though the file exists.

Waiting for 5 seconds, which is the time to live of Tomcat cache entries, and then trying to reference the file will revalidate the cache entry and produce a FileResource instead of EmptyResource in which case the referencing will work normally.

It works in this case

context.getRealPath("/") + "\\bluesModified.wav"

since the path that is getting cached is a directory and the file name is simply concatenated. So the string I have here is just an absolute path to the file I am going to create with no cache entries colliding with it.

My mistake was assuming that getRealPath() is just some "pure" method that will return a string I can use to create files while in fact it has a bit of side effects. These side effects are not documented and even though I might have done some things incorrectly the bottom line is this method is not that predictable to use when doing File IO stuff.

Michael Munta
  • 207
  • 2
  • 16
  • 1
    You have taught me something here. I won't use getRealPath to non-existent files anymore . I will just use context.getRealPath("/") and add on the rest of the path . – rickz Apr 21 '20 at 16:42
1

The String returned by getRealPath from the ServletContext implementation is normalized.

So when you call getRealPath("/") + "\blues.wav") only the String "/" is normalized, and the String concatenation "\blues.wav" is not.

But when you call getRealPath("/" + "\blues.wav")) the full concatened String is normilized.

    public String getRealPath(String path) {
    if ("".equals(path)) {
        path = "/";
    }

    if (this.resources != null) {
        try {
            WebResource resource = this.resources.getResource(path);
            String canonicalPath = resource.getCanonicalPath();
            if (canonicalPath == null) {
                return null;
            }

            if ((resource.isDirectory() && !canonicalPath.endsWith(File.separator) || !resource.exists()) && path.endsWith("/")) {
                return canonicalPath + File.separatorChar;
            }

            return canonicalPath;
        } catch (IllegalArgumentException var4) {
        }
    }

    return null;
}

You can see WebResource resource = this.resources.getResource(path) will try to validate your path and will return a validated path :

    private String validate(String path) {
    if (!this.getState().isAvailable()) {
        throw new IllegalStateException(sm.getString("standardRoot.checkStateNotStarted"));
    } else if (path != null && path.length() != 0 && path.startsWith("/")) {
        String result;
        if (File.separatorChar == '\\') {
            result = RequestUtil.normalize(path, true);
        } else {
            result = RequestUtil.normalize(path, false);
        }

        if (result != null && result.length() != 0 && result.startsWith("/")) {
            return result;
        } else {
            throw new IllegalArgumentException(sm.getString("standardRoot.invalidPathNormal", new Object[]{path, result}));
        }
    } else {
        throw new IllegalArgumentException(sm.getString("standardRoot.invalidPath", new Object[]{path}));
    }
}
bdzzaid
  • 838
  • 8
  • 15
  • 1
    I think you make a good point about normalization. The result of running a program with the first code line at the top of the author's question is C:\Tomcat\webapps\myWebApp\\songModified.wav I see that the backslash is not escaped and we end up with a double backslash in the path. – rickz Apr 20 '20 at 15:46