net.oneandone.sushi.fs.webdav.WebdavNode.java Source code

Java tutorial

Introduction

Here is the source code for net.oneandone.sushi.fs.webdav.WebdavNode.java

Source

/**
 * Copyright 1&1 Internet AG, https://github.com/1and1/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.oneandone.sushi.fs.webdav;

import net.oneandone.sushi.fs.CreateInputStreamException;
import net.oneandone.sushi.fs.CreateOutputStreamException;
import net.oneandone.sushi.fs.DeleteException;
import net.oneandone.sushi.fs.DirectoryNotFoundException;
import net.oneandone.sushi.fs.ExistsException;
import net.oneandone.sushi.fs.FileNotFoundException;
import net.oneandone.sushi.fs.GetLastModifiedException;
import net.oneandone.sushi.fs.LengthException;
import net.oneandone.sushi.fs.ListException;
import net.oneandone.sushi.fs.MkdirException;
import net.oneandone.sushi.fs.MoveException;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.NodeNotFoundException;
import net.oneandone.sushi.fs.SetLastModifiedException;
import net.oneandone.sushi.fs.WriteToException;
import net.oneandone.sushi.fs.webdav.methods.Delete;
import net.oneandone.sushi.fs.webdav.methods.Get;
import net.oneandone.sushi.fs.webdav.methods.Head;
import net.oneandone.sushi.fs.webdav.methods.Method;
import net.oneandone.sushi.fs.webdav.methods.MkCol;
import net.oneandone.sushi.fs.webdav.methods.Move;
import net.oneandone.sushi.fs.webdav.methods.PropFind;
import net.oneandone.sushi.fs.webdav.methods.PropPatch;
import net.oneandone.sushi.fs.webdav.methods.Put;
import net.oneandone.sushi.util.Strings;
import net.oneandone.sushi.util.Util;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.impl.io.ChunkedOutputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.UserPrincipal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

public class WebdavNode extends Node {
    private final WebdavRoot root;

    /**
      * Never starts with a slash.
      * Without type - never with tailing /. With special characters - will be encoded in http requests
      */
    private final String path;

    /**
     * Null or never starts with ?
     */
    private final String encodedQuery;

    private boolean tryDir;

    private final Object tryLock;

    /** @param encodedQuery null or query without initial "?" */
    public WebdavNode(WebdavRoot root, String path, String encodedQuery, boolean tryDir) {
        if (path.startsWith("/")) {
            throw new IllegalArgumentException(path);
        }
        if (encodedQuery != null && encodedQuery.startsWith("?")) {
            throw new IllegalArgumentException(path);
        }
        this.root = root;
        this.path = path;
        this.encodedQuery = encodedQuery;
        this.tryDir = tryDir;
        this.tryLock = new Object();
    }

    public URI getURI() {
        return getURI(root.getFilesystem().getScheme());
    }

    public URI getInternalURI() {
        return getURI(root.getFilesystem().getInternalScheme());
    }

    private URI getURI(String scheme) {
        HttpHost host;

        host = root.host;
        try {
            return new URI(scheme, null, host.getHostName(), host.getPort(), "/" + path, getQuery(), null);
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
    }

    public boolean equals(Object object) {
        if (object instanceof WebdavNode) {
            if (Util.eq(encodedQuery, ((WebdavNode) object).encodedQuery)) {
                return super.equals(object);
            }
        }
        return false;
    }

    @Override
    public WebdavRoot getRoot() {
        return root;
    }

    @Override
    public WebdavNode getParent() {
        return (WebdavNode) doGetParent();
    }

    @Override
    public WebdavNode join(String... paths) {
        return (WebdavNode) doJoin(paths);
    }

    @Override
    public WebdavNode join(List<String> paths) {
        return (WebdavNode) doJoin(paths);
    }

    @Override
    public long length() throws LengthException {
        boolean oldTryDir;
        Property property;

        synchronized (tryLock) {
            oldTryDir = tryDir;
            try {
                tryDir = false;
                property = getProperty(Name.GETCONTENTLENGTH);
            } catch (IOException e) {
                tryDir = oldTryDir;
                throw new LengthException(this, e);
            }
            return Long.parseLong((String) property.getValue());
        }
    }

    private static final SimpleDateFormat FMT;

    static {
        Calendar calendar;

        calendar = Calendar.getInstance();
        calendar.set(2000, Calendar.JANUARY, 1, 0, 0);
        FMT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
        FMT.setTimeZone(TimeZone.getTimeZone("GMT"));
        FMT.set2DigitYearStart(calendar.getTime());
    }

    @Override
    public long getLastModified() throws GetLastModifiedException {
        Property property;

        try {
            synchronized (tryLock) {
                try {
                    property = getProperty(Name.GETLASTMODIFIED);
                } catch (MovedException e) {
                    tryDir = !tryDir;
                    property = getProperty(Name.GETLASTMODIFIED);
                }
            }
        } catch (IOException e) {
            throw new GetLastModifiedException(this, e);
        }
        try {
            synchronized (FMT) {
                return FMT.parse((String) property.getValue()).getTime();
            }
        } catch (ParseException e) {
            throw new GetLastModifiedException(this, e);
        }
    }

    @Override
    public void setLastModified(long millis) throws SetLastModifiedException {
        // no allowed by webdav standard
        throw new SetLastModifiedException(this);
    }

    @Override
    public String getPermissions() {
        throw unsupported("getPermissions()");
    }

    @Override
    public void setPermissions(String permissions) {
        throw unsupported("setPermissions()");
    }

    @Override
    public UserPrincipal getOwner() {
        throw unsupported("getOwner()");
    }

    @Override
    public void setOwner(UserPrincipal owner) {
        throw unsupported("setOwner()");
    }

    @Override
    public GroupPrincipal getGroup() {
        throw unsupported("getGroup()");
    }

    @Override
    public void setGroup(GroupPrincipal group) {
        throw unsupported("setGroup()");
    }

    @Override
    public String getPath() {
        return path;
    }

    public String getQuery() {
        if (encodedQuery != null) {
            try {
                return new URI("foo://bar/path?" + encodedQuery).getQuery();
            } catch (URISyntaxException e) {
                throw new IllegalStateException();
            }
        } else {
            return null;
        }
    }

    @Override
    public Node deleteFile() throws DeleteException, FileNotFoundException {
        try {
            synchronized (tryLock) {
                tryDir = false;
                new Delete(this).invoke();
            }
        } catch (FileNotFoundException e) {
            throw e;
        } catch (MovedException e) {
            throw new FileNotFoundException(this, e);
        } catch (IOException e) {
            throw new DeleteException(this, e);
        }
        return this;
    }

    @Override
    public Node deleteDirectory() throws DirectoryNotFoundException, DeleteException {
        List<WebdavNode> lst;

        try {
            lst = list();
            if (lst == null) {
                throw new DirectoryNotFoundException(this);
            }
            if (lst.size() > 0) {
                throw new DeleteException(this, "directory is not empty");
            }
            synchronized (tryLock) {
                try {
                    new Delete(this).invoke();
                } catch (MovedException e) {
                    tryDir = !tryDir;
                    new Delete(this).invoke();
                }
            }
        } catch (DirectoryNotFoundException | DeleteException e) {
            throw e;
        } catch (IOException e) {
            throw new DeleteException(this, e);
        }
        return this;
    }

    @Override
    public Node deleteTree() throws DeleteException, NodeNotFoundException {
        try {
            synchronized (tryLock) {
                try {
                    new Delete(this).invoke();
                } catch (MovedException e) {
                    tryDir = !tryDir;
                    new Delete(this).invoke();
                }
            }
        } catch (FileNotFoundException e) {
            throw new NodeNotFoundException(this, e);
        } catch (IOException e) {
            throw new DeleteException(this, e);
        }
        return this;
    }

    @Override
    public Node move(Node dest, boolean overwrite) throws MoveException {
        if (dest instanceof WebdavNode) {
            return move((WebdavNode) dest, overwrite);
        } else {
            throw new MoveException(this, dest, "cannot move webdav node to none-webdav node");
        }
    }

    public WebdavNode move(WebdavNode dest, boolean overwrite) throws MoveException {
        try {
            synchronized (tryLock) {
                try {
                    dest.tryDir = tryDir;
                    new Move(this, dest, overwrite).invoke();
                } catch (MovedException e) {
                    tryDir = !tryDir;
                    dest.tryDir = tryDir;
                    new Move(this, dest, overwrite).invoke();
                }
            }
        } catch (IOException e) {
            throw new MoveException(this, dest, e.getMessage(), e);
        }
        return dest;
    }

    @Override
    public WebdavNode mkdir() throws MkdirException {
        try {
            synchronized (tryLock) {
                tryDir = true;
                new MkCol(this).invoke();
            }
        } catch (IOException e) {
            throw new MkdirException(this, e);
        }
        return this;
    }

    @Override
    public void mklink(String target) {
        throw unsupported("mklink()");
    }

    @Override
    public String readLink() {
        throw unsupported("readLink()");
    }

    @Override
    public boolean exists() throws ExistsException {
        synchronized (tryLock) {
            try {
                new Head(this).invoke();
                return true;
            } catch (StatusException e) {
                switch (e.getStatusLine().getStatusCode()) {
                case HttpStatus.SC_MOVED_PERMANENTLY:
                    tryDir = !tryDir;
                    return true;
                case HttpStatus.SC_NOT_FOUND:
                    return false;
                default:
                    throw new ExistsException(this, e);
                }
            } catch (IOException e) {
                throw new ExistsException(this, e);
            }
        }
    }

    @Override
    public boolean isFile() throws ExistsException {
        return tryDir(false);
    }

    @Override
    public boolean isDirectory() throws ExistsException {
        return tryDir(true);
    }

    @Override
    public boolean isLink() {
        return false;
    }

    @Override
    public InputStream createInputStream() throws CreateInputStreamException, FileNotFoundException {
        synchronized (tryLock) {
            tryDir = false;
            try {
                return new Get(this).invoke();
            } catch (FileNotFoundException e) {
                throw e;
            } catch (IOException e) {
                throw new CreateInputStreamException(this, e);
            }
        }
    }

    public long writeTo(OutputStream dest, long skip) throws FileNotFoundException, WriteToException {
        return writeToImpl(dest, skip);
    }

    @Override
    public OutputStream createOutputStream(boolean append)
            throws CreateOutputStreamException, FileNotFoundException {
        byte[] add;
        final Put method;
        final WebdavConnection connection;
        OutputStream result;

        try {
            if (isDirectory()) {
                throw new FileNotFoundException(this);
            }
        } catch (ExistsException e) {
            throw new CreateOutputStreamException(this, e);
        }
        try {
            if (append) {
                try {
                    add = readBytes();
                } catch (FileNotFoundException e) {
                    add = null;
                }
            } else {
                add = null;
            }
            synchronized (tryLock) {
                tryDir = false;
                method = new Put(this);
                connection = method.request();
                result = new ChunkedOutputStream(connection.getOutputBuffer()) {
                    private boolean closed = false;

                    @Override
                    public void close() throws IOException {
                        if (closed) {
                            return;
                        }
                        closed = true;
                        super.close();
                        method.response(connection);
                    }
                };
                if (add != null) {
                    result.write(add);
                }
            }
            return result;
        } catch (IOException e) {
            throw new CreateOutputStreamException(this, e);
        }
    }

    @Override
    public List<WebdavNode> list() throws ListException, DirectoryNotFoundException {
        PropFind method;
        List<WebdavNode> result;
        URI href;

        synchronized (tryLock) {
            try {
                tryDir = true;
                method = new PropFind(this, Name.DISPLAYNAME, 1);
                result = new ArrayList<>();
                for (MultiStatus response : method.invoke()) {
                    try {
                        href = new URI(response.href);
                    } catch (URISyntaxException e) {
                        throw new ListException(this, e);
                    }
                    if (samePath(href)) {
                        // ignore "."
                    } else {
                        result.add(createChild(href));
                    }
                }
                return result;
            } catch (StatusException e) {
                if (e.getStatusLine().getStatusCode() == 400) {
                    return null; // this is a file
                }
                throw new ListException(this, e);
            } catch (MovedException e) {
                tryDir = false;
                return null; // this is a file
            } catch (FileNotFoundException e) {
                throw new DirectoryNotFoundException(this);
            } catch (IOException e) {
                throw new ListException(this, e);
            }
        }
    }

    private boolean samePath(URI uri) {
        String cmp;
        int idx;
        int cl;
        int pl;

        cmp = uri.getPath();
        idx = cmp.indexOf(path);
        if (idx == 1 && cmp.charAt(0) == '/') {
            cl = cmp.length();
            pl = path.length();
            if (cl == 1 + path.length() || cl == 1 + pl + 1 && cmp.charAt(cl - 1) == '/') {
                return true;
            }
        }
        return false;
    }

    private WebdavNode createChild(URI href) {
        String childPath;
        boolean dir;
        WebdavNode result;

        childPath = href.getPath();
        dir = childPath.endsWith("/");
        if (dir) {
            childPath = childPath.substring(0, childPath.length() - 1);
        }
        childPath = Strings.removeLeft(childPath, "/");
        if (!childPath.startsWith(path)) {
            throw new IllegalStateException();
        }
        result = new WebdavNode(root, childPath, null, dir);
        return result;
    }

    public String getAttribute(String name) throws WebdavException {
        Property result;
        Name n;

        n = new Name(name, Method.DAV);
        try {
            synchronized (tryLock) {
                try {
                    result = getPropertyOpt(n);
                } catch (MovedException e) {
                    tryDir = !tryDir;
                    result = getPropertyOpt(n);
                }
            }
            return result == null ? null : (String) result.getValue();
        } catch (IOException e) {
            throw new WebdavException(this, e);
        }
    }

    public void setAttribute(String name, String value) throws WebdavException {
        try {
            setProperty(new Name(name, Method.DAV), value);
        } catch (IOException e) {
            throw new WebdavException(this, e);
        }
    }

    private void setProperty(Name name, String value) throws IOException {
        Property prop;

        prop = new Property(name, value);
        synchronized (tryLock) {
            try {
                new PropPatch(this, prop).invoke();
            } catch (MovedException e) {
                tryDir = !tryDir;
                new PropPatch(this, prop).invoke();
            }
        }
    }

    /** @return never null */
    private Property getProperty(Name name) throws IOException {
        Property result;

        result = getPropertyOpt(name);
        if (result == null) {
            throw new IllegalStateException();
        }
        return result;
    }

    private Property getPropertyOpt(Name name) throws IOException {
        PropFind method;
        List<MultiStatus> response;

        method = new PropFind(this, name, 0);
        response = method.invoke();
        return MultiStatus.lookupOne(response, name).property;
    }

    private boolean tryDir(boolean tryTryDir) throws ExistsException {
        boolean reset;
        boolean result;

        synchronized (tryLock) {
            reset = tryDir;
            tryDir = tryTryDir;
            try {
                if (getRoot().getFilesystem().isDav()) {
                    result = doTryDirDav();
                } else {
                    result = doTryDirHttp();
                }
            } catch (MovedException | FileNotFoundException e) {
                tryDir = reset;
                return false;
            } catch (IOException e) {
                tryDir = reset;
                throw new ExistsException(this, e);
            }
            if (!result) {
                tryDir = reset;
            }
        }
        return result;
    }

    private boolean doTryDirDav() throws IOException {
        Property property;
        org.w3c.dom.Node node;

        property = getProperty(Name.RESOURCETYPE);
        node = (org.w3c.dom.Node) property.getValue();
        if (node == null) {
            return tryDir == false;
        }
        return tryDir == "collection".equals(node.getLocalName());
    }

    private boolean doTryDirHttp() throws IOException {
        try {
            new Head(this).invoke();
            return true;
        } catch (StatusException e2) {
            switch (e2.getStatusLine().getStatusCode()) {
            case HttpStatus.SC_MOVED_PERMANENTLY:
                return false;
            case HttpStatus.SC_NOT_FOUND:
                return false;
            default:
                throw e2;
            }
        }
    }

    //--

    /** see http://tools.ietf.org/html/rfc2616#section-5.1.2 */
    public String getAbsPath() {
        StringBuilder builder;

        synchronized (tryLock) {
            builder = new StringBuilder(path.length() + 10);
            builder.append('/');
            if (!path.isEmpty()) {
                try {
                    builder.append(new URI(null, null, path, null).getRawPath());
                } catch (URISyntaxException e) {
                    throw new IllegalStateException();
                }
                if (tryDir) {
                    builder.append('/');
                }
            }
            if (encodedQuery != null) {
                builder.append('?');
                builder.append(encodedQuery);
            }
        }
        return builder.toString();
    }
}