public class ConfigConnector {
	
	private final static String CONFIG_PART = "ocs/v2.php/apps/provisioning_api/api/v1/config/";
	
	private final ConnectorCommon connectorCommon;
	
	public ConfigConnector(ServerConfig serverConfig){
		this.connectorCommon = new ConnectorCommon(serverConfig);
	}
	
	public List<String> getAppConfigApps(){
		return NextcloudResponseHelper.getAndWrapException(getAppConfigAppsAsync())
			.getAppConfigApps();
	}
	
	private CompletableFuture<AppConfigAppsAnswer> getAppConfigAppsAsync(){
		return connectorCommon.executeGet(CONFIG_PART + "apps", Collections.emptyList(),
			XMLAnswerParser.getInstance(AppConfigAppsAnswer.class));
	}
	
	public List<String> getAppConfigAppKeys(String appConfigApp){
		return NextcloudResponseHelper.getAndWrapException(getAppConfigAppsAsync(appConfigApp))
			.getAppConfigApps();
	}
	
	private CompletableFuture<AppConfigAppsAnswer> getAppConfigAppsAsync(String appConfigApp){
		return connectorCommon.executeGet(CONFIG_PART + "apps/" + appConfigApp,
			Collections.emptyList(), XMLAnswerParser.getInstance(AppConfigAppsAnswer.class));
	}
	
	public String getAppConfigAppKeyValue(String appConfigApp, String appConfigAppKey){
		return NextcloudResponseHelper
			.getAndWrapException(getAppConfigAppsKeyAsync(appConfigApp + "/" + appConfigAppKey))
			.getAppConfigAppKeyValue();
	}
	
	public String getAppConfigAppKeyValue(String appConfigAppKeyPath){
		return NextcloudResponseHelper
			.getAndWrapException(getAppConfigAppsKeyAsync(appConfigAppKeyPath))
			.getAppConfigAppKeyValue();
	}
	
	private CompletableFuture<AppConfigAppKeyValueAnswer> getAppConfigAppsKeyAsync(
		String appConfigAppKeyPath){
		return connectorCommon.executeGet(CONFIG_PART + "apps/" + appConfigAppKeyPath,
			Collections.emptyList(), XMLAnswerParser.getInstance(AppConfigAppKeyValueAnswer.class));
	}
	
	public boolean setAppConfigAppKeyValue(String appConfigApp, String appConfigAppKey,
		Object value){
		return NextcloudResponseHelper.isStatusCodeOkay(
			setAppConfigAppKeyValueAsync(appConfigApp + "/" + appConfigAppKey, value));
	}
	
	public boolean setAppConfigAppKeyValue(String appConfigAppKeyPath, Object value){
		return NextcloudResponseHelper
			.isStatusCodeOkay(setAppConfigAppKeyValueAsync(appConfigAppKeyPath, value));
	}
	
	public CompletableFuture<AppConfigAppKeyValueAnswer> setAppConfigAppKeyValueAsync(
		String appConfigAppKeyPath, Object value){
		List<NameValuePair> postParams = new LinkedList<>();
		postParams.add(new BasicNameValuePair("value", value.toString()));
		return connectorCommon.executePost(CONFIG_PART + "apps/" + appConfigAppKeyPath, postParams,
			XMLAnswerParser.getInstance(AppConfigAppKeyValueAnswer.class));
	}
	
	public boolean deleteAppConfigAppKeyEntry(String appConfigApp, String appConfigAppkey){
		throw new UnsupportedOperationException();
	}
	
} public class FilesharingConnector
{
    private final static String ROOT_PART= "ocs/v1.php/apps/files_sharing/api/v1/";
    private final static String SHARES_PART= ROOT_PART+"shares";

    private final ConnectorCommon connectorCommon;

    public FilesharingConnector(ServerConfig serverConfig) {
        this.connectorCommon = new ConnectorCommon(serverConfig);
    }

    /**
     * Get all shares of this user
     *
     * @return all shares
     */
    public List<Share> getShares()
    {
        return getShares(null, false, false);
    }

    /**
     * Get all shares of this user asynchronously
     *
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<SharesXMLAnswer> getSharesAsync()
    {
        return getSharesAsync(null, false, false);
    }

    /**
     * Gets all shares from a given file/folder
     *
     * @param path      path to file/folder
     * @param reShares  returns not only the shares from the current user but all shares from the given file
     * @param subShares returns all shares within a folder, given that path defines a folder
     * @return matching shares
     */
    public List<Share> getShares(String path, boolean reShares, boolean subShares)
    {
        return NextcloudResponseHelper.getAndCheckStatus(getSharesAsync(path,reShares,subShares)).getShares();
    }

    /**
     * Gets all shares from a given file/folder asynchronously
     *
     * @param path      path to file/folder
     * @param reShares  returns not only the shares from the current user but all shares from the given file
     * @param subShares returns all shares within a folder, given that path defines a folder
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<SharesXMLAnswer> getSharesAsync(String path, boolean reShares, boolean subShares)
    {
        List<NameValuePair> queryParams= new LinkedList<>();
        if (path != null)
        {
            queryParams.add(new BasicNameValuePair("path", path));
        }
        if (reShares)
        {
            queryParams.add(new BasicNameValuePair("reshares", "true"));
        }
        if (subShares)
        {
            queryParams.add(new BasicNameValuePair("subfiles", "true"));
        }
        return connectorCommon.executeGet(SHARES_PART, queryParams, XMLAnswerParser.getInstance(SharesXMLAnswer.class));
    }

    /**
     * Get share info for a single share
     *
     * @param shareId      id of share (Not path of share)
     * @return the share if it has been found, otherwise null
     */
    public Share getShareInfo(int shareId)
    {
        SharesXMLAnswer xa= NextcloudResponseHelper.getAndCheckStatus(getShareInfoAsync(shareId));
        if (xa.getShares() == null)
        {
            return null;
        }
        else if (xa.getShares().size() == 1)
        {
            return xa.getShares().get(0);
        }
        throw new MoreThanOneShareFoundException(shareId);
    }

    /**
     * Get share info for a single share asynchronously
     *
     * @param shareId      id of share (Not path of share)
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<SharesXMLAnswer> getShareInfoAsync(int shareId)
    {
        return connectorCommon.executeGet(SHARES_PART+"/"+Integer.toString(shareId), null, XMLAnswerParser.getInstance(SharesXMLAnswer.class));
    }

    /**
     * Shares the specified path with the provided parameters
     *
     * @param path                  path to the file/folder which should be shared
     * @param shareType             0 = user; 1 = group; 3 = public link; 4 = email; 6 = federated cloud share
     * @param shareWithUserOrGroupIdOrEmail user / group id / email with which the file should be shared
     * @param publicUpload          allow public upload to a public shared folder (true/false)
     * @param password              password to protect public link Share with
     * @param permissions           1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1)
     * @return created share on success
     */
    public Share doShare(
            String path,
            ShareType shareType,
            String shareWithUserOrGroupIdOrEmail,
            Boolean publicUpload,
            String password,
            SharePermissions permissions)
    {
        return NextcloudResponseHelper.getAndCheckStatus(doShareAsync(path, shareType, shareWithUserOrGroupIdOrEmail, publicUpload, password, permissions)).getShare();
    }

    /**
    * Shares the specified path with the provided parameters asynchronously
    *
    * @param path                  path to the file/folder which should be shared
    * @param shareType             0 = user; 1 = group; 3 = public link; 4 = email; 6 = federated cloud share
    * @param shareWithUserOrGroupIdOrEmail user / group id / email with which the file should be shared
    * @param publicUpload          allow public upload to a public shared folder (true/false)
    * @param password              password to protect public link Share with
    * @param permissions           1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1)
    * @return a CompletableFuture containing the result of the operation
    */
    public CompletableFuture<SingleShareXMLAnswer> doShareAsync(
            String path,
            ShareType shareType,
            String shareWithUserOrGroupIdOrEmail,
            Boolean publicUpload,
            String password,
            SharePermissions permissions)
    {
        List<NameValuePair> postParams= new LinkedList<>();
        postParams.add(new BasicNameValuePair("path", path));
        postParams.add(new BasicNameValuePair("shareType", Integer.toString(shareType.getIntValue())));
        postParams.add(new BasicNameValuePair("shareWith", shareWithUserOrGroupIdOrEmail));
        if (publicUpload != null)
        {
            postParams.add(new BasicNameValuePair("publicUpload", publicUpload ? "true" : "false"));
        }
        if (password != null)
        {
            postParams.add(new BasicNameValuePair("password", password));
        }
        if (permissions != null)
        {
            postParams.add(new BasicNameValuePair("permissions", Integer.toString(permissions.getCurrentPermission())));
        }

        return connectorCommon.executePost(SHARES_PART, postParams, XMLAnswerParser.getInstance(SingleShareXMLAnswer.class));
    }

    /**
     * Changes a single attribute of a share
     *
     * @param shareId unique identifier of the share
     * @param key the attribute to change
     * @param value the value to set
     * @return true if the operation succeeded
     */
    public boolean editShare(int shareId, ShareData key, String value)
    {
        return NextcloudResponseHelper.isStatusCodeOkay(editShareAsync(shareId, key, value));
    }

    /**
     * Changes a single attribute of a share asynchronously
     *
     * @param shareId unique identifier of the share
     * @param key the attribute to change
     * @param value the value to set
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<XMLAnswer> editShareAsync(int shareId, ShareData key, String value)
    {
        List<NameValuePair> queryParams= Collections.singletonList(new BasicNameValuePair(key.parameterName, value));
        return connectorCommon.executePut(SHARES_PART, Integer.toString(shareId), queryParams, XMLAnswerParser.getInstance(XMLAnswer.class));
    }

    /**
     * Changes multiple attributes of a share at once
     *
     * @param shareId unique identifier of the share
     * @param values a Map containing the attributes to set
     * @return true if the operation succeeded
     */
    public boolean editShare(int shareId, Map<ShareData,String> values)
    {
        return NextcloudResponseHelper.isStatusCodeOkay(editShareAsync(shareId, values));
    }

    /**
     * Changes multiple attributes of a share at once asynchronously
     *
     * @param shareId unique identifier of the share
     * @param values a Map containing the attributes to set
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<XMLAnswer> editShareAsync(int shareId, Map<ShareData, String> values) {
        List<NameValuePair> queryParams = values.entrySet().stream()
                .map(e -> new BasicNameValuePair(e.getKey().parameterName, e.getValue())).collect(Collectors.toList());
        return connectorCommon.executePut(SHARES_PART, Integer.toString(shareId), queryParams, XMLAnswerParser.getInstance(XMLAnswer.class));
    }

    /**
     * Deletes a share
     *
     * @param shareId unique identifier of the share
     * @return true if the operation succeeded
     */
    public boolean deleteShare(int shareId)
    {
        return NextcloudResponseHelper.isStatusCodeOkay(deleteShareAsync(shareId));
    }

    /**
     * Deletes a share asynchronously
     *
     * @param shareId unique identifier of the share
     * @return a CompletableFuture containing the result of the operation
     */
    public CompletableFuture<XMLAnswer> deleteShareAsync(int shareId)
    {
        return connectorCommon.executeDelete(SHARES_PART, Integer.toString(shareId), null, XMLAnswerParser.getInstance(XMLAnswer.class));
    }
}
 @XmlAccessorType(XmlAccessType.FIELD)
public class Share
{
    private int         id;
    @XmlElement(name = "share_type")
    private ShareType   shareType;
    @XmlElement(name = "uid_owner")
    private String      ownerId;
    @XmlElement(name = "displayname_owner")
    private String      ownerDisplayName;
    @XmlElement(name = "permissions")
    @XmlJavaTypeAdapter(SharePermissionsAdapter.class)
    private SharePermissions    sharePermissions;
    @XmlElement(name = "uid_file_owner")
    private String      fileOwnerId;
    @XmlElement(name = "displayname_file_owner")
    private String      fileOwnerDisplayName;
    private String      path;
    @XmlElement(name = "item_type")
    private ItemType    itemType;
    @XmlElement(name = "file_target")
    private String      fileTarget;
    @XmlElement(name = "share_with")
    private String      shareWithId;
    @XmlElement(name = "share_with_displayname")
    private String      shareWithDisplayName;
    private String      token;
    @XmlElement(name = "stime")
    @XmlJavaTypeAdapter(InstantXmlAdapter.class)
    private Instant     shareTime;
    @XmlJavaTypeAdapter(LocalDateXmlAdapter.class)
    private LocalDate   expiration;
    private String url;
    private String mimetype;

    public int getId() {
        return id;
    }

    public ShareType getShareType() {
        return shareType;
    }

    public String getOwnerId() {
        return ownerId;
    }

    public String getOwnerDisplayName() {
        return ownerDisplayName;
    }

    public SharePermissions getSharePermissions() {
        return sharePermissions;
    }

    public String getFileOwnerId() {
        return fileOwnerId;
    }

    public String getFileOwnerDisplayName() {
        return fileOwnerDisplayName;
    }

    public String getPath() {
        return path;
    }

    public ItemType getItemType() {
        return itemType;
    }

    public String getFileTarget() {
        return fileTarget;
    }

    public String getShareWithId() {
        return shareWithId;
    }

    public String getShareWithDisplayName() {
        return shareWithDisplayName;
    }

    public String getToken() {
        return token;
    }

    public Instant getShareTime() {
        return shareTime;
    }

    public LocalDate getExpiration() {
        return expiration;
    }

    public String getUrl() {
        return url;
    }

    public String getMimetype() {
        return mimetype;
    }

    private static final class SharePermissionsAdapter extends XmlAdapter<Integer, SharePermissions>
    {
        @Override
        public Integer marshal(SharePermissions sharePermissions)
        {
            return sharePermissions.getCurrentPermission();
        }

        @Override
        public SharePermissions unmarshal(Integer v)
        {
            return new SharePermissions(v);
        }
    }
} public class SharePermissions {

    public enum SingleRight {
        READ(1),
        UPDATE(2),
        CREATE(4),
        DELETE(8),
        SHARE(16);

        private final int intValue;

        private SingleRight(int iV) {
            intValue= iV;
        }

        public int getIntValue() {
            return intValue;
        }
    }

    private final int currentPermission;

    public SharePermissions(int currentPermission) {
        this.currentPermission = currentPermission;
    }

    public SharePermissions(SingleRight... permissions) {
        int calculatedPermission = 0;
        for(SingleRight permission: permissions)
        {
            calculatedPermission += permission.getIntValue();
        }
        this.currentPermission = calculatedPermission;
    }

    public boolean hasAllRights()
    {
        return currentPermission == (
                SingleRight.READ.getIntValue()+
                SingleRight.UPDATE.getIntValue()+
                SingleRight.CREATE.getIntValue()+
                SingleRight.DELETE.getIntValue()+
                SingleRight.SHARE.getIntValue()
                );
    }

    public boolean hasRight(SingleRight permission) {
        return (currentPermission & permission.intValue) != 0;
    }

    public int getCurrentPermission() {
        return currentPermission;
    }

    @Override
    public String toString() {
        return Integer.toString(currentPermission);
    }
} @XmlType
@XmlEnum(Integer.class)
public enum ShareType {

    @XmlEnumValue("0") USER(0),
    @XmlEnumValue("1") GROUP(1),
    @XmlEnumValue("3") PUBLIC_LINK(3),
    @XmlEnumValue("4") EMAIL(4),
    @XmlEnumValue("6") FEDERATED_CLOUD_SHARE(6);

    private final int intValue;
    
    private ShareType(int iV) {
        intValue= iV;
    }

    public int getIntValue() {
        return intValue;
    }

    public static ShareType getShareTypeForIntValue(int i)
    {
        for (ShareType s : ShareType.values())
        {
            if (s.getIntValue() == i)
            {
                return s;
            }
        }
        throw new InvalidParameterException("Invalid ShareType found " + i);
    }
    
} @XmlRootElement(name = "ocs")
public class SharesXMLAnswer extends XMLAnswer
{
    @XmlElementWrapper(name = "data")
    @XmlElement(name = "element")
    private List<Share> shares;

    public List<Share> getShares() {
        return shares;
    }
} @XmlRootElement(name = "ocs")
public class SingleShareXMLAnswer extends XMLAnswer
{
	@XmlElement(name = "data")
    private Share share= null;

    public Share getShare() {
        return share;
    }
} public class Files extends AWebdavHandler{
    private static final Logger LOG = LoggerFactory.getLogger(Files.class);

    public Files(ServerConfig serverConfig) {
        super(serverConfig);
    }

    /**
     * method to check if a file already exists
     *
     * @param remotePath path of the file
     * @return boolean value if the given file exists or not
     */
    public boolean fileExists(String remotePath) {
        return pathExists(remotePath);
    }

    /**
     * Uploads a file at the specified path with the data from the File
     *
     * @param localSource file which should be uploaded
     * @param remotePath path where the file should be uploaded to
     */
    public void uploadFile(File localSource, String remotePath) {
        String path = buildWebdavPath(remotePath);
        Sardine sardine = buildAuthSardine();

        try
        {
            sardine.put(path, localSource, null, true);
        } catch (IOException e)
        {
            throw new NextcloudApiException(e);
        }   
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch(Exception ex2)
            {
                LOG.warn("Error in sardine shutdown", ex2);
            }
        }
    }
    
    /**
     * Uploads a file at the specified path with the data from the InputStream
     *
     * @param inputStream InputStream of the file which should be uploaded
     * @param remotePath path where the file should be uploaded to
     */
    public void uploadFile(InputStream inputStream, String remotePath) {
        uploadFile(inputStream, remotePath, true);
    }

    /**
     * Uploads a file at the specified path with the data from the InputStream
     * with an additional continue header
     *
     * @param inputStream InputStream of the file which should be uploaded
     * @param remotePath path where the file should be uploaded to
     * @param continueHeader Continue header is added to receive a possible error by the server before any data is sent.
     */
    public void uploadFile(InputStream inputStream, String remotePath, boolean continueHeader) {
        String path = buildWebdavPath(remotePath);
        
        Sardine sardine = buildAuthSardine();

        try
        {
            sardine.put(path, inputStream, null, continueHeader);
        } catch (IOException e)
        {
            throw new NextcloudApiException(e);
        }        
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch(Exception ex2)
            {
                LOG.warn("Error in sardine shutdown", ex2);
            }
        }
    }

    /**
     * method to remove files
     *
     * @param remotePath path of the file which should be removed
     */
    public void removeFile(String remotePath) {
        deletePath(remotePath);
    }

    /**
     * method to rename/move files
     *
     * @param oldPath path of the file which should be renamed/moved
     * @param newPath path of the file which should be renamed/moved
     * @param overwriteExisting overwrite if target already exists
     */
    public void renameFile(String oldPath, String newPath, boolean overwriteExisting) {
        renamePath(oldPath, newPath, overwriteExisting);
    }

    
    /**
     * Downloads the file at the specified remotepath to the download directory,
     * and returns true if the download is successful
     *
     * @param remotePath Remotepath where the file is saved in the nextcloud
     * server
     * @param downloadDirPath Path where the file is downloaded, it would be
     * created if it doesn't exist.
     * @return boolean
     * @throws IOException  In case of IO errors
     */
    public boolean downloadFile(String remotePath, String downloadDirPath) throws IOException {
        boolean status = false;
        String path = buildWebdavPath(remotePath);
        Sardine sardine = buildAuthSardine();

        File downloadFilepath = new File(downloadDirPath);
        if (!downloadFilepath.exists())
        {
            downloadFilepath.mkdir();
        }

        if (fileExists(remotePath))
        {
            //Extract the Filename from the path
            String[] segments = remotePath.split("/");
            String filename = segments[segments.length - 1];
            downloadDirPath = downloadDirPath + "/" + filename;
        }
        InputStream in = null;
        try
        {
            in = sardine.get(path);
            byte[] buffer = new byte[AWebdavHandler.FILE_BUFFER_SIZE];
            int bytesRead;
            File targetFile = new File(downloadDirPath);
            try (OutputStream outStream = new FileOutputStream(targetFile))
            {
                while ((bytesRead = in.read(buffer)) != -1)
                {
                    outStream.write(buffer, 0, bytesRead);
                }
                outStream.flush();
                outStream.close();
            }
            status = true;
        } catch (IOException e)
        {
            throw new NextcloudApiException(e);
        } finally
        {
            sardine.shutdown();
            if (in != null)
            {
                in.close();
            }
        }
        return status;
    }

    /**
     * Downloads the file at the specified remotepath as an InputStream,
     *
     * @param remotePath Remotepath where the file is saved in the nextcloud
     * server
     * @return InputStream
     * @throws IOException  In case of IO errors
     */
    public InputStream downloadFile(String remotePath) throws IOException {
        String path = buildWebdavPath(remotePath);
        Sardine sardine = buildAuthSardine();

        WebdavInputStream in = null;
        try
        {
            in = new WebdavInputStream(sardine, sardine.get(path));
        } catch (IOException e)
        {
            throw new NextcloudApiException(e);
        }
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch(Exception ex2)
            {
                LOG.warn("Error in sardine shutdown", ex2);
            }
        }
        return in;
    }
    
    /**
     * Get the properties of a resource, File or Folder
     *
     * @param remotePath Remotepath of the resource to query for
     * server
     * @param allProperties Return all properties, not only base properties
     * https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html#requesting-properties
     * @return InputStream
     * @throws IOException  In case of IO errors
     */
    public ResourceProperties getProperties(String remotePath, boolean allProperties) throws IOException {
        String path = buildWebdavPath(remotePath);
        Sardine sardine = buildAuthSardine();

        try
        {
            Set<QName> props= new HashSet<>();
            if (allProperties)
            {
                props.add(new QName("DAV:", "getlastmodified", "d"));
                props.add(new QName("DAV:", "getetag", "d"));
                props.add(new QName("DAV:", "getcontenttype", "d"));
                props.add(new QName("DAV:", "resourcetype", "d"));
                props.add(new QName("DAV:", "getcontentlength", "d"));
                props.add(new QName("DAV:", "displayname", "d"));
                props.add(new QName("http://owncloud.org/ns", "id", "oc"));
                props.add(new QName("http://owncloud.org/ns", "fileid", "oc"));
                props.add(new QName("http://owncloud.org/ns", "favorite", "oc"));
                props.add(new QName("http://owncloud.org/ns", "comments-href", "oc"));
                props.add(new QName("http://owncloud.org/ns", "comments-count", "oc"));
                props.add(new QName("http://owncloud.org/ns", "comments-unread", "oc"));
                props.add(new QName("http://owncloud.org/ns", "owner-id", "oc"));
                props.add(new QName("http://owncloud.org/ns", "owner-display-name", "oc"));
                props.add(new QName("http://owncloud.org/ns", "share-types", "oc"));
                props.add(new QName("http://owncloud.org/ns", "checksums", "oc"));
                props.add(new QName("http://nextcloud.org/ns", "has-preview", "nc"));
                props.add(new QName("http://owncloud.org/ns", "permissions", "oc"));
                props.add(new QName("http://owncloud.org/ns", "size", "oc"));
            }
            List<DavResource> resources= sardine.propfind(path, 0, props);
            if (resources != null && resources.size() == 1)
            {
                DavResource res= resources.get(0);
                ResourceProperties retVal= new ResourceProperties();
                retVal.setModified(res.getModified());
                retVal.setEtag(res.getEtag());
                retVal.setContentType(res.getContentType());
                retVal.setContentLength(res.getContentLength());
                retVal.setCreation(res.getCreation());
                retVal.setDisplayName(res.getDisplayName());
                if (allProperties)
                {
                    Map<String, String> custProps= res.getCustomProps();
                    retVal.setResourceType(custProps.get("resourcetype"));
                    retVal.setId(custProps.get("id"));
                    retVal.setFileId(custProps.get("fileid"));
                    retVal.setFavorite("1".equals(custProps.get("favorite")));
                    retVal.setCommentsHref(custProps.get("comments-href"));
                    retVal.setCommentsCount( convertStringToLong(custProps.get("comments-count")));
                    retVal.setCommentsUnread(convertStringToLong(custProps.get("comments-unread")));
                    retVal.setOwnerId(custProps.get("owner-id"));
                    retVal.setOwnerDisplayName(custProps.get("owner-display-name"));
                    retVal.setShareTypes(custProps.get("share-types"));
                    retVal.setChecksums(custProps.get("checksums"));
                    retVal.setHasPreview("1".equals(custProps.get("has-preview")));
                    retVal.setPermissions(custProps.get("permissions"));
                    retVal.setSize(convertStringToLong(custProps.get("size")));
                }
                return retVal;
            }
            else
            {
                throw new NextcloudApiException("Unexpected number of resources received");
            }
        } catch (IOException e)
        {
            throw new NextcloudApiException(e);
        }
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch(Exception ex2)
            {
                LOG.warn("Error in sardine shutdown", ex2);
            }
        }
    }
    
    private long convertStringToLong(String number)
    {
        if (number == null || number.equals(""))
        {
            return 0;
        }
        else
        {
            return Long.parseLong(number);
        }
    }
} public class Folders extends AWebdavHandler{

    private static final Logger LOG = LoggerFactory.getLogger(Folders.class);

    public Folders(ServerConfig serverConfig) {
        super(serverConfig);
    }

    /**
     * Get all subfolders of the specified path
     *
     * @param remotePath path of the folder
     * @return found subfolders
     *
     * @deprecated The methods naming is somehow misleading, as it lists
     * all resources (subfolders and files) within the given {@code rootPath}.
     * Please use {@link #listFolderContent(String)} instead.
     */
    @Deprecated
    public List<String> getFolders(String remotePath)
    {
        return listFolderContent(remotePath);
    }

    /**
     * List all file names and subfolders of the specified path
     *
     * @param remotePath path of the folder
     * @return found file names and subfolders
     */
    public List<String> listFolderContent(String remotePath)
    {
        return listFolderContent(remotePath, 1);
    }

    /**
     * List all file names and subfolders of the specified path traversing
     * into subfolders to the given depth.
     *
     * @param remotePath path of the folder
     * @param depth depth of recursion while listing folder contents
     * @return found file names and subfolders
     */
    public List<String> listFolderContent(String remotePath, int depth)
    {
        return listFolderContent(remotePath, depth, false, false);
    }

    /**
     * List all file names and subfolders of the specified path traversing
     * into subfolders to the given depth.
     *
     * @param remotePath path of the folder
     * @param depth depth of recursion while listing folder contents
     * @param excludeFolderNames excludes the folder names from the list
     * @param returnFullPath return full path to files, not only filename
     *                       like folder1/folder1/file.txt
     * @return found file names and subfolders
     */
    public List<String> listFolderContent(String remotePath, int depth, boolean excludeFolderNames, boolean returnFullPath)
    {
        String pathPrefix = getWebdavPathPrefix();
        String path = buildWebdavPath(remotePath);

        List<String> retVal = new LinkedList<>();
        Sardine sardine = buildAuthSardine();
        List<DavResource> resources;
        try {
            resources = sardine.list(path, depth);
        } catch (IOException e) {
            throw new NextcloudApiException(e);
        }
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch (IOException ex)
            {
                LOG.warn("error in closing sardine connector", ex);
            }
        }
        for (DavResource res : resources)
        {
            if (excludeFolderNames && res.isDirectory()) {
                // Dont' return folders
            }
            else {
                if (returnFullPath)
                {
                    if (res.getPath().startsWith(pathPrefix))
                    {
                        retVal.add(res.getPath().substring(pathPrefix.length()));
                    }
                    else
                    {
                        LOG.error("Unhandled edge case, path prefix {} does not match built prefix {}", res.getPath(), path);
                        retVal.add(res.getPath());
                    }
                }
                else
                {
                    retVal.add(res.getName());
                }
            }
        }
        return retVal;
    }

    /**
     * Checks if the folder at the specified path exists
     *
     * @param remotePath path of the folder
     * @return true if the folder exists
     */
    public boolean exists(String remotePath)
    {
        return pathExists(remotePath);
    }

    /**
     * Creates a folder at the specified path
     *
     * @param remotePath path of the folder
     */
    public void createFolder(String remotePath)
    {
        String path=  buildWebdavPath(remotePath );
        Sardine sardine = buildAuthSardine();

        try {
            sardine.createDirectory(path);
        } catch (IOException e) {
            throw new NextcloudApiException(e);
        }
        finally
        {
            try
            {
                sardine.shutdown();
            }
            catch (IOException ex)
            {
                LOG.warn("error in closing sardine connector", ex);
            }
        }
    }

    /**
     * Deletes the folder at the specified path
     *
     * @param remotePath path of the folder
     */
    public void deleteFolder(String remotePath)
    {
        deletePath(remotePath);
    }

    /**
     * method to rename/move folder
     *
     * @param oldPath path of the folder which should be renamed/moved
     * @param newPath path of the folder which should be renamed/moved
     * @param overwriteExisting overwrite if target already exists
     */
    public void renameFolder(String oldPath, String newPath, boolean overwriteExisting) {
        renamePath(oldPath, newPath, overwriteExisting);
    }

    /**
     * Downloads the folder at the specified remotePath to the rootDownloadDirPath
     *
     * @param remotePath the path in the nextcloud server with respect to the specific folder
     * @param rootDownloadDirPath the local path in the system where the folder needs be saved
     * @throws IOException  In case of IO errors
     */
    public void downloadFolder(String remotePath, String rootDownloadDirPath) throws IOException {
        int depth=1;
        String[] segments = remotePath.split("/");
        String folderName = segments[segments.length - 1];
        String newDownloadDir = rootDownloadDirPath + "/" + folderName;
        File nefile1 = new File(newDownloadDir);
        if(!nefile1.exists()) {
            LOG.info("Creating new download directory: "+newDownloadDir);
            nefile1.mkdir();
        }

        String listPathURL = buildWebdavPath(remotePath);
        int count = 0;
        String filePath;
        List<String> retVal= new LinkedList<>();
        List<DavResource> resources;
        Sardine sardine = buildAuthSardine();
        try
        {
            try {
                resources = sardine.list(listPathURL, depth);
            } catch (IOException e) {
                throw new NextcloudApiException(e);
            }

            for (DavResource res : resources)
            {
                System.out.println(res.getName());
                //Skip the Documents folder which is listed as default as first by the sardine output
                if(count != 0) {
                    if(res.isDirectory()) {
                        String fileName = res.getName();
                        String pathtosend = remotePath + "/" + fileName;
                        downloadFolder(pathtosend,newDownloadDir);
                    }
                    else {
                            String fileName = res.getName();
                            filePath = buildWebdavPath(remotePath + "/" + fileName);
                            retVal.add(res.getName());

                            InputStream in = null;
                            if (sardine.exists(filePath)) {
                                in = sardine.get(filePath);
                                byte[] buffer = new byte[AWebdavHandler.FILE_BUFFER_SIZE];
                                int bytesRead;
                                File targetFile = new File(newDownloadDir + "/" + fileName);
                                try (OutputStream outStream = new FileOutputStream(targetFile))
                                {
                                    while ((bytesRead = in.read(buffer)) != -1)
                                    {
                                        outStream.write(buffer, 0, bytesRead);
                                    }
                                    outStream.flush();
                                    outStream.close();
                                }
                            }
                    }
                }
                count ++;
            }
        }
        finally {
            try
            {
                sardine.shutdown();
            }
            catch (IOException ex)
            {
                LOG.warn("error in closing sardine connector", ex);
            }
        }
    }
} public class ResourceProperties {
    private long    contentLength= -1;
    private String  contentType= null;
    private Date    creation= null;
    private String  displayName= null;
    private String  etag= null;
    private Date    modified= null;
    
    private String resourceType;
    private String id;
    private String fileId;
    private boolean favorite;
    private String  commentsHref;
    private long    commentsCount;
    private long    commentsUnread;
    private String  ownerId;
    private String  ownerDisplayName;
    private String  shareTypes;
    private String  permissions;
    private String  checksums;
    private boolean hasPreview;
    private long    size; // Unlike contentlength, this property also works for folders reporting the size of everything in the folder

    public ResourceProperties() {
    }

    
    /**
     * @return the modified
     */
    public Date getModified() {
        return modified;
    }

    /**
     * @param modified the modified to set
     */
    public void setModified(Date modified) {
        this.modified = modified;
    }

    /**
     * @return the contentType
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * @param contentType the contentType to set
     */
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    /**
     * @return the etag
     */
    public String getEtag() {
        return etag;
    }

    /**
     * @param etag the etag to set
     */
    public void setEtag(String etag) {
        this.etag = etag;
    }

    /**
     * @return the displayName
     */
    public String getDisplayName() {
        return displayName;
    }

    /**
     * @param displayName the displayName to set
     */
    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    /**
     * @return the contentLength
     */
    public long getContentLength() {
        return contentLength;
    }

    /**
     * @param contentLength the contentLength to set
     */
    public void setContentLength(long contentLength) {
        this.contentLength = contentLength;
    }

    /**
     * @return the creation
     */
    public Date getCreation() {
        return creation;
    }

    /**
     * @param creation the creation to set
     */
    public void setCreation(Date creation) {
        this.creation = creation;
    }

    /**
     * @return the resourceType
     */
    public String getResourceType() {
        return resourceType;
    }

    /**
     * @param resourceType the resourceType to set
     */
    public void setResourceType(String resourceType) {
        this.resourceType = resourceType;
    }

    /**
     * @return the id
     */
    public String getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * @return the fileId
     */
    public String getFileId() {
        return fileId;
    }

    /**
     * @param fileId the fileId to set
     */
    public void setFileId(String fileId) {
        this.fileId = fileId;
    }

    /**
     * @return the favorite
     */
    public boolean isFavorite() {
        return favorite;
    }

    /**
     * @param favorite the favorite to set
     */
    public void setFavorite(boolean favorite) {
        this.favorite = favorite;
    }

    /**
     * @return the commentsHref
     */
    public String getCommentsHref() {
        return commentsHref;
    }

    /**
     * @param commentsHref the commentsHref to set
     */
    public void setCommentsHref(String commentsHref) {
        this.commentsHref = commentsHref;
    }

    /**
     * @return the commentsCount
     */
    public long getCommentsCount() {
        return commentsCount;
    }

    /**
     * @param commentsCount the commentsCount to set
     */
    public void setCommentsCount(long commentsCount) {
        this.commentsCount = commentsCount;
    }

    /**
     * @return the commentsUnread
     */
    public long getCommentsUnread() {
        return commentsUnread;
    }

    /**
     * @param commentsUnread the commentsUnread to set
     */
    public void setCommentsUnread(long commentsUnread) {
        this.commentsUnread = commentsUnread;
    }

    /**
     * @return the ownerId
     */
    public String getOwnerId() {
        return ownerId;
    }

    /**
     * @param ownerId the ownerId to set
     */
    public void setOwnerId(String ownerId) {
        this.ownerId = ownerId;
    }

    /**
     * @return the ownerDisplayName
     */
    public String getOwnerDisplayName() {
        return ownerDisplayName;
    }

    /**
     * @param ownerDisplayName the ownerDisplayName to set
     */
    public void setOwnerDisplayName(String ownerDisplayName) {
        this.ownerDisplayName = ownerDisplayName;
    }

    /**
     * @return the shareTypes
     */
    public String getShareTypes() {
        return shareTypes;
    }

    /**
     * @param shareTypes the shareTypes to set
     */
    public void setShareTypes(String shareTypes) {
        this.shareTypes = shareTypes;
    }

    /**
     * @return the checksums
     */
    public String getChecksums() {
        return checksums;
    }

    /**
     * @param checksums the checksums to set
     */
    public void setChecksums(String checksums) {
        this.checksums = checksums;
    }

    /**
     * @return the hasPreview
     */
    public boolean isHasPreview() {
        return hasPreview;
    }

    /**
     * @param hasPreview the hasPreview to set
     */
    public void setHasPreview(boolean hasPreview) {
        this.hasPreview = hasPreview;
    }

    /**
     * @return the size
     */
    public long getSize() {
        return size;
    }

    /**
     * @param size the size to set
     */
    public void setSize(long size) {
        this.size = size;
    }

    /**
     * @return the permissions
     */
    public String getPermissions() {
        return permissions;
    }

    /**
     * @param permissions the permissions to set
     */
    public void setPermissions(String permissions) {
        this.permissions = permissions;
    }
} public class NextcloudVersion
{

    private final static String PROPERTIES_PATH_PATTERN = "/org/aarboard/nextcloud/api/webdav/pathresolver/webdavpathresolver_{0}.properties";
    private final static String APPEND_USERNAME_PATTERN = "nextcloud.webdav.base.{0}.suffix.append.username";

    private final String versionValue;

    /**
     * defaults to 20
     */
    private Integer compatibleVersion = 20;
    private String major = "0";
    private String minor = "0";
    private String patch = "0";
    private String revision = "0";

    private Properties configProperties = new Properties();
    /**
     * defaults to files
     */
    private WebDavPathResolverBuilder.TYPE webdavType = WebDavPathResolverBuilder.TYPE.FILES;

    NextcloudVersion(String versionValue)
    {
        this.versionValue = versionValue;
        paresVersionString(versionValue);
        loadValues();
    }

    /**
     * static create factory method
     * Value need to be of form
     * major.minor.path(.revision) E.g. 20.4.0.0 or 14.1.3
     *
     * @param value of NextCloudInstance
     * @return a versionbn instance
     * @since 11.5
     */
    public static NextcloudVersion get(final String value)
    {
        if (null == value)
        {
            throw new IllegalArgumentException("Version value cannot be null !");
        }

        if (value.trim().isEmpty())
        {
            throw new IllegalArgumentException("Version value cannot be empty !");
        }

        return new NextcloudVersion(value);
    }

    public NextcloudVersion ofType(WebDavPathResolverBuilder.TYPE _type)
    {
        this.webdavType = _type;

        return this;
    }

    public String getWebdavBasePath()
    {
        return configProperties.getProperty("nextcloud.webdav.base.path");
    }

    /**
     *
     * @return true if usernae shall appendend to path, false otherwise
     */
    public boolean isAppendUserName()
    {
        return Boolean.parseBoolean(configProperties.getProperty(MessageFormat.format(APPEND_USERNAME_PATTERN, this.webdavType.name().toLowerCase()), "false"));
    }

    /**
     *
     * @return true if appendSuffix shall appendend to path, false otherwise
     */
    public boolean isAppendSuffix()
    {
        return Boolean.parseBoolean(configProperties.getProperty(MessageFormat.format(APPEND_USERNAME_PATTERN, this.webdavType.name().toLowerCase()), "false"));
    }

    @Override
    public String toString()
    {
        return major + "." + minor + "." + patch + "." + revision + ", compatible with [" + compatibleVersion + "]";
    }

    private void paresVersionString(String value)
    {
        String[] values = value.split("\\.");
        if (values.length > 0)
        {
            major = values[0];
            compatibleVersion = Integer.parseInt(major);
        }

        if (values.length > 1)
        {
            minor = values[1];
        }

        if (values.length > 2)
        {
            patch = values[2];
        }

        if (values.length > 3)
        {
            revision = values[3];
        }

        if (compatibleVersion > 14)
        {
            compatibleVersion = 20;
        }
        else
        {
            compatibleVersion = 14;
        }

    }

    private void loadValues()
    {
        try (InputStream inStream = getClass().getResourceAsStream(MessageFormat.format(PROPERTIES_PATH_PATTERN, compatibleVersion.toString())))
        {
            configProperties.load(inStream);
        }
        catch (IOException ex)
        {
            throw new NextcloudApiException(ex);
        }
    }
} public interface WebDavPathResolver
{

    /**
     * Calculates the webdav server path. E.g.:
     * nextcloud/remote.php/dav/files/username
     * nextcloud/remote.php/dav/files/username/myfolder
     *
     * @param remotePaths Subpath elements to append
     * @return the webdav path
     */
    String getWebDavPath(String... remotePaths);

} public class WebDavPathResolverBuilder
{

    public final static String NEXTCLOUD_WEBDAV_BASE_PATH = "nextcloud.webdav.base.path";

    public final static String NEXTCLOUD_WEBDAV_BASE_PATH_SUFFIX = "nextcloud.webdav.base.suffix.path";

    public final static String NEXTCLOUD_WEBDAV_BASE_PATH_PREFIX = "nextcloud.webdav.base.prefix.path";

    public final static String NEXTCLOUD_USER_NAME = "nextcloud.userName";

    /**
     * Type of the resolver FILES, VCARD, CALDAV, VERSION
     */
    public enum TYPE
    {
        /**
         * specifies to calculate the files path of the nextcloud instance
         */
        FILES("files"),
        /**
         * VCARD path
         */
        VCARD("addressbooks/users"),
        /**
         * CALDAV
         */
        CALDAV("calendars"),
        /**
         * specifies to calculate the Version path of the nextcloud instance
         */
        VERSION("");

        private final String suffix;

        private TYPE(String suffix)
        {
            this.suffix = suffix;
        }

        public String getSuffix()
        {
            return suffix;
        }

    }

    private WebDavPathResolver result;

    private TYPE type;

    /**
     * Defaults to the latest
     */
    private NextcloudVersion version = NextcloudVersion.get("20.0.4");

    private Map<String, String> valueMap = new HashMap<>();

    public static WebDavPathResolverBuilder get(final TYPE type)
    {
        return new WebDavPathResolverBuilder(type);
    }

    WebDavPathResolverBuilder(final TYPE type)
    {
        this.type = type;
    }

    /**
     *
     * @param version of the nextcloud instance
     * @return the WebDavPathResolverBuilder
     */
    public WebDavPathResolverBuilder ofVersion(NextcloudVersion version)
    {
        this.version = version.ofType(type);
        return this;
    }

    /**
     * The remoteuser
     *
     * @param user the remoteuser (Must be the username and not 
     *             the login name if they are not identical)
     * @return the WebDavPathResolverBuilder
     */
    public WebDavPathResolverBuilder withUserName(String user)
    {
        if (null != user)
        {
            valueMap.put(NEXTCLOUD_USER_NAME, user);
        }
        return this;
    }

    /**
     * E.g. nextcloud
     *
     * @param suffix of the basepath
     * @return the WebDavPathResolverBuilder
     */
    public WebDavPathResolverBuilder withBasePathSuffix(String suffix)
    {
        if (null != suffix)
        {
            valueMap.put(NEXTCLOUD_WEBDAV_BASE_PATH_SUFFIX, suffix);
        }

        return this;
    }

    /**
     * E.g. files,calendars
     *
     * @param prefix the suffix of the base path
     * @return the WebDavPathResolverBuilder
     */
    public WebDavPathResolverBuilder withBasePathPrefix(String prefix)
    {
        if (null != prefix)
        {
            valueMap.put(NEXTCLOUD_WEBDAV_BASE_PATH_PREFIX, prefix);
        }

        return this;
    }

    /**
     * E.g. remote.php/dav
     *
     * @param path of the nextcloud webdav interface
     * @return the WebDavPathResolverBuilder
     */
    public WebDavPathResolverBuilder withBasePath(String path)
    {
        if (null == path)
        {
            throw new IllegalArgumentException("WebDav base path cannot be null!");
        }

        valueMap.put(NEXTCLOUD_WEBDAV_BASE_PATH, path);
        return this;
    }

    /**
     * builds the resolver
     *
     * @return the resolver
     * @see WebDavPathResolver
     */
    public WebDavPathResolver build()
    {
        switch (type)
        {
            case FILES:
            case CALDAV:
            case VCARD:
                result = new FolderWebDavPathResolverImpl(valueMap.getOrDefault(NEXTCLOUD_WEBDAV_BASE_PATH, version.getWebdavBasePath()))
                        .setPathPrefix(valueMap.getOrDefault(NEXTCLOUD_WEBDAV_BASE_PATH_PREFIX, ""))
                        .setPathSuffix(version.isAppendSuffix() ? valueMap.getOrDefault(NEXTCLOUD_WEBDAV_BASE_PATH_SUFFIX, type.getSuffix()) : "")
                        .setUserName(version.isAppendUserName() ? valueMap.getOrDefault(NEXTCLOUD_USER_NAME, "") : "");

                break;
            case VERSION:
                result = new WebDavpathResolverVersionImpl(valueMap.getOrDefault(NEXTCLOUD_WEBDAV_BASE_PATH, "status.php"))//
                        .setPathPrefix(valueMap.getOrDefault(NEXTCLOUD_WEBDAV_BASE_PATH_PREFIX, ""));
                break;
            default:
                result = null;
                break;

        }

        return result;
    }

    private static class FolderWebDavPathResolverImpl implements WebDavPathResolver
    {

        private String webDavBasePath;
        private String userName;
        private String pathSuffix;
        private String pathPrefix;
        private String webdavPath = null;

        public FolderWebDavPathResolverImpl(String webDavBasePath)
        {
            this.webDavBasePath = webDavBasePath;
            this.userName = "";
            this.pathSuffix = "";
            this.pathPrefix = "";
        }

        private FolderWebDavPathResolverImpl setPathPrefix(String pathPrefix)
        {
            this.pathPrefix = pathPrefix;
            return this;
        }

        private FolderWebDavPathResolverImpl setWebDavBasePath(String webDavBasePath)
        {
            this.webDavBasePath = webDavBasePath;
            return this;
        }

        private FolderWebDavPathResolverImpl setUserName(String userName)
        {
            this.userName = userName;
            return this;
        }

        private FolderWebDavPathResolverImpl setPathSuffix(String pathSuffix)
        {
            this.pathSuffix = pathSuffix;
            return this;
        }

        private String getWebdavPath()
        {
            if (null == this.webdavPath)
            {
                // We can't use Paths here, since we need / as separator, and not a platform specific delimiter
                // which might be something else
                this.webdavPath= PathHelper.concatPathElements(
                        true,
                        this.pathPrefix,
                        this.webDavBasePath,
                        this.pathSuffix,
                        this.userName);
            }
            return this.webdavPath;
        }

        @Override
        public String getWebDavPath(String ... remotePath)
        {
            return PathHelper.concatPathElements(true, getWebdavPath(), remotePath);
        }
    }

}
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import org.aarboard.nextcloud.api.config.ConfigConnector;
import org.aarboard.nextcloud.api.filesharing.FilesharingConnector;
import org.aarboard.nextcloud.api.filesharing.Share;
import org.aarboard.nextcloud.api.filesharing.SharePermissions;
import org.aarboard.nextcloud.api.filesharing.ShareType;
import org.aarboard.nextcloud.api.filesharing.SharesXMLAnswer;
import org.aarboard.nextcloud.api.filesharing.SingleShareXMLAnswer;
import org.aarboard.nextcloud.api.provisioning.*;
import org.aarboard.nextcloud.api.utils.*;
import org.aarboard.nextcloud.api.webdav.Files;
import org.aarboard.nextcloud.api.webdav.Folders;
import org.aarboard.nextcloud.api.webdav.ResourceProperties;
import org.aarboard.nextcloud.api.webdav.pathresolver.NextcloudVersion;
import org.aarboard.nextcloud.api.webdav.pathresolver.WebDavPathResolver;
import org.aarboard.nextcloud.api.webdav.pathresolver.WebDavPathResolverBuilder;
/*
 * Copyright (C) 2017 a.schild
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.aarboard.nextcloud.api;


public class NextcloudConnector {

    private final ServerConfig _serverConfig;
    private final ProvisionConnector pc;
    private final FilesharingConnector fc;
    private final ConfigConnector cc;
    private final Folders fd;