Context getRealPath() for non-existent file

Viewed 281

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

}
2 Answers

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.

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}));
    }
}
Related