From ba39cbb1f371746eb182801d460b020f04495f93 Mon Sep 17 00:00:00 2001
From: umjammer <umjammer@gmail.com>
Date: Sat, 9 May 2020 01:22:47 +0900
Subject: [PATCH] make it thread safe

---
 pom.xml                                       |   2 +-
 .../core/filters/WebdavForBrowserFilter.java  |  13 ++
 src/main/java/vavi/apps/webdav/Config.java    | 101 ++++++--
 src/main/java/vavi/apps/webdav/Main.java      |   8 +-
 .../java/vavi/apps/webdav/SecurityConfig.java |  11 +
 .../java/vavi/net/webdav/MyBeanFactory.java   |  65 ++++++
 .../java/vavi/net/webdav/SqlStrageDao.java    |  27 ++-
 .../java/vavi/net/webdav/WebdavService.java   | 221 +++++-------------
 .../webdav/auth/BasicWebTokenRefresher.java   |  10 +-
 .../net/webdav/auth/box/BoxWebOAuth2.java     |   8 +-
 .../webdav/auth/dropbox/DropBoxWebOAuth2.java |  11 +-
 src/main/resources/templates/index.html       |   3 +
 src/main/resources/templates/onedrive.html    |   4 +-
 src/test/java/Test2.java                      |  30 ++-
 14 files changed, 300 insertions(+), 214 deletions(-)
 create mode 100644 src/main/java/vavi/net/webdav/MyBeanFactory.java

diff --git a/pom.xml b/pom.xml
index 71fef1f..4b72589 100644
--- a/pom.xml
+++ b/pom.xml
@@ -127,7 +127,7 @@ $ git push heroku master &amp;&amp; heroku logs -t
     <dependency>
       <groupId>com.github.umjammer</groupId>
       <artifactId>vavi-apps-fuse</artifactId>
-      <version>0.0.8</version>
+      <version>0.0.9</version>
       <exclusions>
         <exclusion>
           <groupId>org.apache.jackrabbit</groupId>
diff --git a/src/main/java/org/cryptomator/webdav/core/filters/WebdavForBrowserFilter.java b/src/main/java/org/cryptomator/webdav/core/filters/WebdavForBrowserFilter.java
index f4d4c88..c1f5c51 100644
--- a/src/main/java/org/cryptomator/webdav/core/filters/WebdavForBrowserFilter.java
+++ b/src/main/java/org/cryptomator/webdav/core/filters/WebdavForBrowserFilter.java
@@ -1,3 +1,9 @@
+/*
+ * Copyright (c) 2020 by Naohide Sano, All rights reserved.
+ *
+ * Programmed by Naohide Sano
+ */
+
 package org.cryptomator.webdav.core.filters;
 
 import java.io.IOException;
@@ -11,6 +17,13 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+
+/**
+ * WebdavForBrowserFilter.
+ *
+ * @author <a href="mailto:umjammer@gmail.com">Naohide Sano</a> (umjammer)
+ * @version 0.00 2020/05/08 umjammer initial version <br>
+ */
 public class WebdavForBrowserFilter implements HttpFilter {
 
     private static final Logger LOG = LoggerFactory.getLogger(WebdavForBrowserFilter.class);
diff --git a/src/main/java/vavi/apps/webdav/Config.java b/src/main/java/vavi/apps/webdav/Config.java
index 258bd59..7acfae9 100644
--- a/src/main/java/vavi/apps/webdav/Config.java
+++ b/src/main/java/vavi/apps/webdav/Config.java
@@ -6,7 +6,13 @@
 
 package vavi.apps.webdav;
 
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
 import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.sql.DataSource;
 
@@ -15,13 +21,18 @@
 import org.cryptomator.webdav.core.filters.UnicodeResourcePathNormalizationFilter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.boot.web.servlet.ServletRegistrationBean;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
 import org.springframework.security.web.firewall.HttpFirewall;
 import org.springframework.security.web.firewall.StrictHttpFirewall;
 
+import com.github.fge.filesystem.box.provider.BoxFileSystemProvider;
+import com.github.fge.fs.dropbox.provider.DropBoxFileSystemProvider;
 import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariDataSource;
 
@@ -29,6 +40,8 @@
 import vavi.net.webdav.SqlStrageDao;
 import vavi.net.webdav.WebdavService;
 import vavi.net.webdav.auth.StrageDao;
+import vavi.net.webdav.auth.WebOAuth2;
+import vavi.net.webdav.auth.WebUserCredential;
 import vavi.net.webdav.auth.box.BoxWebAppCredential;
 import vavi.net.webdav.auth.box.BoxWebOAuth2;
 import vavi.net.webdav.auth.dropbox.DropBoxWebAppCredential;
@@ -37,6 +50,8 @@
 import vavi.net.webdav.auth.google.GoogleWebAuthenticator;
 import vavi.net.webdav.auth.microsoft.MicrosoftWebAppCredential;
 import vavi.net.webdav.auth.microsoft.MicrosoftWebOAuth2;
+import vavi.nio.file.googledrive.GoogleDriveFileSystemProvider;
+import vavi.nio.file.onedrive4.OneDriveFileSystemProvider;
 
 
 /**
@@ -46,8 +61,11 @@
  * @version 0.00 2020/05/05 umjammer initial version <br>
  */
 @Configuration
+@ComponentScan("vavi.net.webdav")
 public class Config {
 
+//    private static final Logger LOG = LoggerFactory.getLogger(Config.class);
+
     @Value("${spring.datasource.url}")
     private String dbUrl;
 
@@ -129,28 +147,79 @@ public DropBoxWebAppCredential dropboxAppCredential() {
     }
 
     @Bean
-    public MicrosoftWebOAuth2 microsoftWebOAuth2(@Autowired MicrosoftWebAppCredential microsoftAppCredential) {
-        return new MicrosoftWebOAuth2(microsoftAppCredential);
-    }
-
-    @Bean
-    public GoogleWebAuthenticator googleWebOAuth2(@Autowired GoogleWebAppCredential googleAppCredential) {
-        return new GoogleWebAuthenticator(googleAppCredential);
+    public WebdavService webdavService() {
+        return new WebdavService();
     }
 
-    @Bean
-    public BoxWebOAuth2 boxWebOAuth2(@Autowired BoxWebAppCredential boxAppCredential) {
-        return new BoxWebOAuth2(boxAppCredential);
+    @Autowired
+    MicrosoftWebAppCredential microsoftAppCredential;
+    @Autowired
+    GoogleWebAppCredential googleAppCredential;
+    @Autowired
+    BoxWebAppCredential boxAppCredential;
+    @Autowired
+    DropBoxWebAppCredential dropboxAppCredential;
+
+    @Bean
+    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+    public WebOAuth2<?, ?> getWebOAuth2(String scheme) throws IOException {
+        switch (scheme) {
+        case "onedrive":
+            return new MicrosoftWebOAuth2(microsoftAppCredential);
+        case "googledrive":
+            return new GoogleWebAuthenticator(googleAppCredential);
+        case "box":
+            return new BoxWebOAuth2(boxAppCredential);
+        case "dropbox":
+            return new DropBoxWebOAuth2(dropboxAppCredential);
+        default:
+            throw new IllegalArgumentException("unsupported scheme: " + scheme);
+        }
     }
 
+    /**
+     * @param id 'scheme:id' i.e. 'onedrive:foo@bar.com'
+     */
     @Bean
-    public DropBoxWebOAuth2 dropBoxWebOAuth2(@Autowired DropBoxWebAppCredential dropboxAppCredential) {
-        return new DropBoxWebOAuth2(dropboxAppCredential);
-    }
+    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+    public FileSystem getFileSystem(String id) throws IOException {
+        String[] part1s = id.split(":");
+        if (part1s.length < 2) {
+            throw new IllegalArgumentException("bad 2nd path component: should be 'scheme:id' i.e. 'onedrive:foo@bar.com'");
+        }
+        String scheme = part1s[0];
+        String idForScheme = part1s[1];
+
+        URI uri = URI.create(scheme + ":///");
+        Map<String, Object> env = new HashMap<>();
+        switch (scheme) {
+        case "onedrive":
+            env.put(OneDriveFileSystemProvider.ENV_APP_CREDENTIAL, microsoftAppCredential);
+            env.put(OneDriveFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
+            env.put("ignoreAppleDouble", true);
+            break;
+        case "googledrive":
+            env.put(GoogleDriveFileSystemProvider.ENV_APP_CREDENTIAL, googleAppCredential);
+            env.put(GoogleDriveFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
+            env.put("ignoreAppleDouble", true);
+            break;
+        case "box":
+            env.put(BoxFileSystemProvider.ENV_APP_CREDENTIAL, boxAppCredential);
+            env.put(BoxFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
+            env.put("ignoreAppleDouble", true);
+            break;
+        case "dropbox":
+            env.put(DropBoxFileSystemProvider.ENV_APP_CREDENTIAL, dropboxAppCredential);
+            env.put(DropBoxFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
+            env.put("ignoreAppleDouble", true);
+            break;
+        default:
+            throw new IllegalArgumentException("unsupported scheme: " + scheme);
+        }
 
-    @Bean
-    public WebdavService webdavService() {
-        return new WebdavService();
+        // https://github.com/spring-projects/spring-boot/issues/7110#issuecomment-252247036
+        FileSystem fs = FileSystems.newFileSystem(uri, env, Thread.currentThread().getContextClassLoader());
+        return fs;
     }
 }
 
diff --git a/src/main/java/vavi/apps/webdav/Main.java b/src/main/java/vavi/apps/webdav/Main.java
index 1ec21a0..eb21056 100644
--- a/src/main/java/vavi/apps/webdav/Main.java
+++ b/src/main/java/vavi/apps/webdav/Main.java
@@ -87,7 +87,7 @@ String microsoft(Model model,
         } else {
             model.addAttribute("error", error);
             model.addAttribute("errorDescription", errorDescription);
-            return "onedrive";
+            return "app_error";
         }
     }
 
@@ -103,7 +103,7 @@ String box(Model model,
         } else {
             model.addAttribute("error", error);
             model.addAttribute("errorDescription", errorDescription);
-            return "onedrive";
+            return "app_error";
         }
     }
 
@@ -119,7 +119,7 @@ String dropbox(Model model,
         } else {
             model.addAttribute("error", error);
             model.addAttribute("errorDescription", errorDescription);
-            return "onedrive";
+            return "app_error";
         }
     }
 
@@ -135,7 +135,7 @@ String google(Model model,
         } else {
             model.addAttribute("error", error);
             model.addAttribute("errorDescription", errorDescription);
-            return "onedrive";
+            return "app_error";
         }
     }
 }
diff --git a/src/main/java/vavi/apps/webdav/SecurityConfig.java b/src/main/java/vavi/apps/webdav/SecurityConfig.java
index 7571b5e..10a5305 100644
--- a/src/main/java/vavi/apps/webdav/SecurityConfig.java
+++ b/src/main/java/vavi/apps/webdav/SecurityConfig.java
@@ -1,3 +1,8 @@
+/*
+ * Copyright (c) 2019 by Naohide Sano, All rights reserved.
+ *
+ * Programmed by Naohide Sano
+ */
 
 package vavi.apps.webdav;
 
@@ -10,6 +15,12 @@
 import vavi.net.webdav.JavaFsWebDavServlet;
 
 
+/**
+ * SecurityConfig.
+ *
+ * @author <a href="mailto:umjammer@gmail.com">Naohide Sano</a> (umjammer)
+ * @version 0.00 2020/05/08 umjammer initial version <br>
+ */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
diff --git a/src/main/java/vavi/net/webdav/MyBeanFactory.java b/src/main/java/vavi/net/webdav/MyBeanFactory.java
new file mode 100644
index 0000000..5883dc8
--- /dev/null
+++ b/src/main/java/vavi/net/webdav/MyBeanFactory.java
@@ -0,0 +1,65 @@
+/*
+ * https://stackoverflow.com/questions/12537851/accessing-spring-beans-in-static-method
+ */
+
+package vavi.net.webdav;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import vavi.net.webdav.auth.OAuthException;
+import vavi.net.webdav.auth.WebOAuth2;
+
+
+/**
+ * gather spring dependencies.
+ *
+ * @author <a href="mailto:umjammer@gmail.com">Naohide Sano</a> (umjammer)
+ * @version 0.00 2020/05/08 umjammer initial version <br>
+ */
+@Service
+public class MyBeanFactory implements InitializingBean {
+    private static MyBeanFactory instance;
+
+    @Autowired
+    BeanFactory beanFactory;
+
+    private FileSystem getFileSystemInternal(String id) throws IOException {
+        try {
+            FileSystem fs = beanFactory.getBean(FileSystem.class, id);
+            return fs;
+        } catch (BeanCreationException e) {
+            OAuthException t = findRootCause(e, OAuthException.class);
+            throw t != null ? t : e;
+        }
+    }
+
+    private  <T extends Throwable> T findRootCause(Throwable t, Class<T> clazz) {
+        while (t.getCause() != null) {
+            t = t.getCause();
+            if (clazz.isInstance(t)) {
+                return clazz.cast(t);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        instance = this;
+    }
+
+    public static FileSystem getFileSystem(String id) throws IOException {
+        return instance.getFileSystemInternal(id);
+    }
+
+    public static WebOAuth2<?, ?> getWebOAuth2(String scheme) {
+        return instance.beanFactory.getBean(WebOAuth2.class, scheme);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/vavi/net/webdav/SqlStrageDao.java b/src/main/java/vavi/net/webdav/SqlStrageDao.java
index e632491..cc516bf 100644
--- a/src/main/java/vavi/net/webdav/SqlStrageDao.java
+++ b/src/main/java/vavi/net/webdav/SqlStrageDao.java
@@ -17,6 +17,8 @@
 
 import javax.sql.DataSource;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import vavi.net.webdav.auth.StrageDao;
@@ -30,7 +32,7 @@
  */
 public class SqlStrageDao implements StrageDao {
 
-//    private static final Logger LOG = LoggerFactory.getLogger(PostgressStrageDao.class);
+    private static final Logger LOG = LoggerFactory.getLogger(SqlStrageDao.class);
 
     @Autowired
     private DataSource dataSource;
@@ -53,8 +55,10 @@ public List<String> load() {
     @Override
     public void update(String id, String token) {
         try (Connection connection = dataSource.getConnection()) {
-            Statement stmt = connection.createStatement();
-            stmt.executeUpdate("UPDATE credential SET token = '" + token + "' WHERE id = '" + id + "'");
+            PreparedStatement pstmt = connection.prepareStatement("UPDATE credential SET token = ? WHERE id = ?");
+            pstmt.setString(1, token);
+            pstmt.setString(2, id);
+            pstmt.execute();
         } catch (SQLException e) {
             throw new IllegalStateException(e);
         }
@@ -63,9 +67,11 @@ public void update(String id, String token) {
     @Override
     public String select(String id) {
         try (Connection connection = dataSource.getConnection()) {
-            Statement stmt = connection.createStatement();
-            ResultSet rs = stmt.executeQuery("SELECT token FROM credential WHERE id = '" + id + "'");
+            PreparedStatement pstmt = connection.prepareStatement("SELECT token FROM credential WHERE id = ?");
+            pstmt.setString(1, id);
+            ResultSet rs = pstmt.executeQuery();
             if (rs.next()) {
+LOG.debug("[" + id + "]: " + rs.getString("token"));
                 return rs.getString("token");
             } else {
                 return null;
@@ -75,11 +81,14 @@ public String select(String id) {
         }
     }
 
+    private static final String googleId = "StoredCredential";
+
     @Override
     public byte[] selectGoogle() {
         try (Connection connection = dataSource.getConnection()) {
-            Statement stmt = connection.createStatement();
-            ResultSet rs = stmt.executeQuery("SELECT * FROM google WHERE id = 'StoredCredential'");
+            PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM google WHERE id = ?");
+            pstmt.setString(1, googleId);
+            ResultSet rs = pstmt.executeQuery();
 
             if (rs.next()) {
                 return rs.getBytes("credentials");
@@ -94,9 +103,9 @@ public byte[] selectGoogle() {
     public void updateGoogle(byte[] credentials) {
         try (Connection connection = dataSource.getConnection()) {
             connection.setAutoCommit(false);
-            PreparedStatement pstmt = connection
-                    .prepareStatement("UPDATE google SET credentials = ? WHERE id = 'StoredCredential'");
+            PreparedStatement pstmt = connection.prepareStatement("UPDATE google SET credentials = ? WHERE id = ?");
             pstmt.setBytes(1, credentials);
+            pstmt.setString(2, googleId);
             pstmt.execute();
             connection.commit();
         } catch (SQLException e) {
diff --git a/src/main/java/vavi/net/webdav/WebdavService.java b/src/main/java/vavi/net/webdav/WebdavService.java
index a32f825..28a6197 100644
--- a/src/main/java/vavi/net/webdav/WebdavService.java
+++ b/src/main/java/vavi/net/webdav/WebdavService.java
@@ -8,8 +8,6 @@
 
 import java.io.IOException;
 import java.net.URI;
-import java.net.URLDecoder;
-import java.nio.charset.Charset;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
@@ -22,28 +20,12 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 
-import com.github.fge.filesystem.box.provider.BoxFileSystemProvider;
-import com.github.fge.fs.dropbox.provider.DropBoxFileSystemProvider;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-
-import vavi.net.auth.oauth2.AppCredential;
 import vavi.net.http.HttpUtil;
 import vavi.net.webdav.auth.OAuthException;
 import vavi.net.webdav.auth.StrageDao;
 import vavi.net.webdav.auth.WebOAuth2;
-import vavi.net.webdav.auth.WebUserCredential;
-import vavi.net.webdav.auth.box.BoxWebAppCredential;
-import vavi.net.webdav.auth.box.BoxWebOAuth2;
-import vavi.net.webdav.auth.dropbox.DropBoxWebAppCredential;
-import vavi.net.webdav.auth.dropbox.DropBoxWebOAuth2;
-import vavi.net.webdav.auth.google.GoogleWebAppCredential;
-import vavi.net.webdav.auth.google.GoogleWebAuthenticator;
-import vavi.net.webdav.auth.microsoft.MicrosoftWebAppCredential;
-import vavi.net.webdav.auth.microsoft.MicrosoftWebOAuth2;
 import vavi.nio.file.gathered.GatheredFileSystemProvider;
-import vavi.nio.file.googledrive.GoogleDriveFileSystemProvider;
-import vavi.nio.file.onedrive4.OneDriveFileSystemProvider;
+import vavi.nio.file.gathered.NameMap;
 
 
 /**
@@ -56,89 +38,67 @@ public class WebdavService {
 
     private static final Logger LOG = LoggerFactory.getLogger(WebdavService.class);
 
-    /** */
-    public static final String URL_ROOT_PATH = "webdav";
-
-    /** <scheme, Path> */
-    private Map<String, Path> rootPaths = new HashMap<>();
-
-    /** */
-    private Path gatheredFsRoot;
+    @Autowired
+    private StrageDao dao;
 
     /** <id, FileSystem> */
     private Map<String, FileSystem> fileSystems = new HashMap<>();
 
-    /** <scheme, BasicAppCredential> */
-    private Map<String, AppCredential> appCredentials = new HashMap<>();
+    /** <id, String> */
+    private NameMap nameMap = new NameMap();
 
-    /** <scheme, WebOAuth2> */
-    private Map<String, WebOAuth2<?, ?>> oauth2s = new HashMap<>();
+    /** */
+    private FileSystem registerFileSystem(String id) throws IOException {
+        FileSystem fs = MyBeanFactory.getFileSystem(id);
+        fileSystems.put(id, fs);
+        nameMap.put(id, id.replaceAll("[:@\\.]", "_"));
+        return fs;
+    }
 
-    @Autowired
-    MicrosoftWebAppCredential microsoftAppCredential;
-    @Autowired
-    GoogleWebAppCredential googleAppCredential;
-    @Autowired
-    BoxWebAppCredential boxAppCredential;
-    @Autowired
-    DropBoxWebAppCredential dropboxAppCredential;
+    /** */
+    private void deregisterFileSystem(String id) {
+        fileSystems.remove(id);
+        nameMap.remove(id);
+    }
 
-    @Autowired
-    MicrosoftWebOAuth2 microsoftWebOAuth2;
-    @Autowired
-    GoogleWebAuthenticator googleWebOAuth2;
-    @Autowired
-    BoxWebOAuth2 boxWebOAuth2;
-    @Autowired
-    DropBoxWebOAuth2 dropBoxWebOAuth2;
-
-    private boolean inited = false;
-
-    /** @see #inited */
-    private void preInit() {
-        if (!inited) {
-            appCredentials.put("onedrive", microsoftAppCredential);
-            appCredentials.put("box", boxAppCredential);
-            appCredentials.put("dropbox", dropboxAppCredential);
-            appCredentials.put("googledrive", googleAppCredential);
-
-appCredentials.forEach((k, v) -> System.err.println(k + ": " + v));
-
-            oauth2s.put("onedrive", microsoftWebOAuth2);
-            oauth2s.put("googledrive", googleWebOAuth2);
-            oauth2s.put("box", boxWebOAuth2);
-            oauth2s.put("dropbox", dropBoxWebOAuth2);
-
-oauth2s.forEach((k, v) -> System.err.println(k + ": " + v));
-
-            Map<String, String> nameMap = new HashMap<>();
-            this.nameMap = HashBiMap.create(nameMap);
-
-            dao.load().forEach(id -> {
-                try {
-LOG.debug("add: " + id);
-                    FileSystem fs = getFileSystem(id);
-                    fileSystems.put(id, fs);
-                } catch (OAuthException e) {
-LOG.info("locked: " + id + ": " + e.getMessage());
-                } catch (Exception e) {
-LOG.warn(id, e);
+    /**
+     * should call after di
+     */
+    private Map<String, OAuthException> prepareFileSystems() {
+
+        Map<String, OAuthException> errors= new HashMap<>();
+
+        dao.load().forEach(id -> {
+            try {
+LOG.debug("ADD: " + id + "    ---------------------------------------------------");
+                if (!fileSystems.containsKey(id)) {
+                    registerFileSystem(id);
                 }
-            });
+            } catch (OAuthException e) {
+LOG.info("NOT LOGINED: " + id + ": " + e.getMessage());
+                errors.put(id, e);
+                deregisterFileSystem(id);
+            } catch (Exception e) {
+LOG.warn(id, e);
+                deregisterFileSystem(id);
+            }
+        });
 
-            inited = true;
-        }
+        return errors;
     }
 
-    /** <id, String> */
-    private BiMap<String, String> nameMap;
+    /** */
+    public static final String URL_ROOT_PATH = "webdav";
 
-    @Autowired
-    private StrageDao dao;
+    /** <scheme, Path> */
+    private Map<String, Path> rootPaths = new HashMap<>();
+
+    /** */
+    private Path gatheredFsRoot;
 
     /** */
     public void init() throws IOException {
-        preInit();
+        prepareFileSystems();
 
         URI uri = URI.create("gatheredfs:///");
         Map<String, Object> env = new HashMap<>();
@@ -149,18 +109,6 @@ public void init() throws IOException {
         gatheredFsRoot = fs.getRootDirectories().iterator().next();
     }
 
-    /**
-     * @param path apparent name
-     * @return real id
-     */
-    private String decode(String path) throws IOException {
-        if (nameMap.size() > 0) {
-            return nameMap.inverse().get(path);
-        } else {
-            return URLDecoder.decode(path, Charset.forName("utf-8").name());
-        }
-    }
-
     /** */
     public Path resolve(String relativeUrl) throws IOException {
         String[] parts = relativeUrl.split("/");
@@ -172,7 +120,7 @@ public Path resolve(String relativeUrl) throws IOException {
             return gatheredFsRoot;
         }
 
-        String id = decode(parts[1]);
+        String id = nameMap.decodeFsName(parts[1]);
 System.err.println(id);
         String path = String.join("/", Arrays.copyOfRange(parts, 2, parts.length));
 
@@ -184,7 +132,8 @@ public Path resolve(String relativeUrl) throws IOException {
             if (fileSystems.containsKey(id)) {
                 fs = fileSystems.get(id);
             } else {
-                fs = getFileSystem(id);
+                fs = registerFileSystem(id);
+                // TODO assert false "???";
             }
             rootPath = fs.getRootDirectories().iterator().next();
             if (!Files.exists(rootPath)) {
@@ -197,73 +146,11 @@ public Path resolve(String relativeUrl) throws IOException {
         return rootPath.resolve(path);
     }
 
-    /**
-     * @param id "scheme:your@id.com"
-     */
-    private FileSystem getFileSystem(String id) throws IOException {
-        String[] part1s = id.split(":");
-        if (part1s.length < 2) {
-            throw new IllegalArgumentException("bad 2nd path component: should be 'scheme:id' i.e. 'onedrive:foo@bar.com'");
-        }
-        String scheme = part1s[0];
-        String idForScheme = part1s[1];
-
-        URI uri = URI.create(scheme + ":///");
-        Map<String, Object> env = new HashMap<>();
-        switch (scheme) {
-        case "onedrive":
-            env.put(OneDriveFileSystemProvider.ENV_APP_CREDENTIAL, appCredentials.get(scheme));
-            env.put(OneDriveFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
-            env.put("ignoreAppleDouble", true);
-            break;
-        case "googledrive":
-            env.put(GoogleDriveFileSystemProvider.ENV_APP_CREDENTIAL, appCredentials.get(scheme));
-            env.put(GoogleDriveFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
-            env.put("ignoreAppleDouble", true);
-            break;
-        case "box":
-            env.put(BoxFileSystemProvider.ENV_APP_CREDENTIAL, appCredentials.get(scheme));
-            env.put(BoxFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
-            env.put("ignoreAppleDouble", true);
-            break;
-        case "dropbox":
-            env.put(DropBoxFileSystemProvider.ENV_APP_CREDENTIAL, appCredentials.get(scheme));
-            env.put(DropBoxFileSystemProvider.ENV_USER_CREDENTIAL, new WebUserCredential(idForScheme));
-            env.put("ignoreAppleDouble", true);
-            break;
-        case "vfs":
-        default:
-            throw new IllegalArgumentException("unsupported scheme: " + scheme);
-        }
-
-        nameMap.put(id, id.replaceAll("[:@\\.]", "_"));
-
-        // https://github.com/spring-projects/spring-boot/issues/7110#issuecomment-252247036
-        FileSystem fs = FileSystems.newFileSystem(uri, env, Thread.currentThread().getContextClassLoader());
-        return fs;
-    }
-
     /** for view:/admin/list */
     public Map<?, ?>[] getStrageStatus() {
-        preInit();
-
-        Map<String, OAuthException> errors= new HashMap<>();
-
-        dao.load().forEach(id -> {
-            try {
-LOG.debug("LIST: " + id + "    ---------------------------------------------------");
-                if (!fileSystems.containsKey(id)) {
-                    fileSystems.put(id, getFileSystem(id));
-                }
-            } catch (OAuthException e) {
-LOG.debug("NOT LOGINED: " + id);
-                errors.put(id, e);
-            } catch (Exception e) {
-                LOG.warn(id, e);
-            }
-        });
+        Map<String, OAuthException> errors = prepareFileSystems();
 
-        return new Map[] { fileSystems, errors, nameMap };
+        return new Map[] { fileSystems, errors, nameMap.map() };
     }
 
     /** <state, WebOAuth2> */
@@ -280,7 +167,7 @@ public String login(String id) {
             String[] parts = id.split(":");
             String scheme = parts[0];
             String idForScheme = parts[1];
-            WebOAuth2<?, ?> oauth2 = oauth2s.get(scheme);
+            WebOAuth2<?, ?> oauth2 = MyBeanFactory.getWebOAuth2(scheme);
             URI uri = oauth2.getAuthorizationUrl();
 LOG.debug("authorizationUrl: " + uri);
             String state = HttpUtil.splitQuery(uri).get("state")[0];
@@ -302,6 +189,8 @@ public void auth(String code, String state) {
             String id = sessionIds.get(state);
             oauth2.setResult(code, state);
             oauth2.authorize(id);
+            sessionOAuth2s.remove(state);
+            sessionIds.remove(state);
 LOG.debug("auth done: " + id);
         } catch (IOException e) {
             throw new IllegalStateException(e);
diff --git a/src/main/java/vavi/net/webdav/auth/BasicWebTokenRefresher.java b/src/main/java/vavi/net/webdav/auth/BasicWebTokenRefresher.java
index b7d33f8..d7b7841 100644
--- a/src/main/java/vavi/net/webdav/auth/BasicWebTokenRefresher.java
+++ b/src/main/java/vavi/net/webdav/auth/BasicWebTokenRefresher.java
@@ -9,9 +9,11 @@
 import java.io.IOException;
 import java.util.function.Supplier;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import vavi.net.auth.oauth2.AppCredential;
 import vavi.net.auth.oauth2.BaseTokenRefresher;
-import vavi.util.Debug;
 
 
 /**
@@ -22,6 +24,8 @@
  */
 public class BasicWebTokenRefresher extends BaseTokenRefresher<String> {
 
+    private static final Logger LOG = LoggerFactory.getLogger(BasicWebTokenRefresher.class);
+
     private String schemeId;
 
     private WebAppCredential appCredential;
@@ -35,14 +39,14 @@ public BasicWebTokenRefresher(AppCredential appCredential, String id, Supplier<L
 
     @Override
     public void writeRefreshToken(String refreshToken) throws IOException {
-Debug.println("save refreshToken: " + refreshToken);
+LOG.debug("save refreshToken [" + schemeId + "]: " + refreshToken);
         appCredential.getStrageDao().update(schemeId, refreshToken);
     }
 
     @Override
     public String readRefreshToken() throws IOException {
         String refreshToken = appCredential.getStrageDao().select(schemeId);
-Debug.println("load refreshToken: " + refreshToken);
+LOG.debug("load refreshToken [" + schemeId + "]: " + refreshToken);
         return refreshToken;
     }
 
diff --git a/src/main/java/vavi/net/webdav/auth/box/BoxWebOAuth2.java b/src/main/java/vavi/net/webdav/auth/box/BoxWebOAuth2.java
index 7ca3976..878fe43 100644
--- a/src/main/java/vavi/net/webdav/auth/box/BoxWebOAuth2.java
+++ b/src/main/java/vavi/net/webdav/auth/box/BoxWebOAuth2.java
@@ -10,11 +10,13 @@
 import java.net.URI;
 import java.util.Arrays;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.box.sdk.BoxAPIConnection;
 
 import vavi.net.webdav.auth.WebAppCredential;
 import vavi.net.webdav.auth.WebOAuth2;
-import vavi.util.Debug;
 import vavi.util.StringUtil;
 
 
@@ -28,6 +30,8 @@
  */
 public class BoxWebOAuth2 implements WebOAuth2<String, BoxAPIConnection> {
 
+    private static final Logger LOG = LoggerFactory.getLogger(BoxWebOAuth2.class);
+
     /** */
     private WebAppCredential appCredential;
 
@@ -47,7 +51,7 @@ public BoxAPIConnection authorize(String id) throws IOException {
         api.setAutoRefresh(true);
 
         String save = api.save();
-Debug.println("save: " + save);
+LOG.debug("save [" + appCredential.getScheme() + ":" + id + "]: " + save);
         appCredential.getStrageDao().update(appCredential.getScheme() + ":" + id, save);
 
         return api;
diff --git a/src/main/java/vavi/net/webdav/auth/dropbox/DropBoxWebOAuth2.java b/src/main/java/vavi/net/webdav/auth/dropbox/DropBoxWebOAuth2.java
index b9d734b..4b1c76a 100644
--- a/src/main/java/vavi/net/webdav/auth/dropbox/DropBoxWebOAuth2.java
+++ b/src/main/java/vavi/net/webdav/auth/dropbox/DropBoxWebOAuth2.java
@@ -8,7 +8,6 @@
 
 import java.io.IOException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.Locale;
 
 import javax.servlet.http.HttpSession;
@@ -53,7 +52,7 @@ public class DropBoxWebOAuth2 implements WebOAuth2<String, String> {
     private DbxWebAuth webAuth;
 
     /** */
-    private String authorizeUrl;
+    private URI authorizeUrl;
 
     /** */
     private String redirectUri;
@@ -77,7 +76,7 @@ public DropBoxWebOAuth2(WebAppCredential appCredential) {
         webAuth = new DbxWebAuth(requestConfig, appInfo);
         Request request = Request.newBuilder().withRedirectUri(appCredential.getRedirectUrl(), csrfTokenStore).build();
 
-        authorizeUrl = webAuth.authorize(request);
+        authorizeUrl = URI.create(webAuth.authorize(request));
     }
 
     /** */
@@ -97,11 +96,7 @@ public String authorize(String id) throws IOException {
 
     @Override
     public URI getAuthorizationUrl() {
-        try {
-            return new URI(authorizeUrl);
-        } catch (URISyntaxException e) {
-            throw new IllegalStateException(e);
-        }
+        return authorizeUrl;
     }
 
     @Override
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index 78b65bf..b494b3c 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -57,4 +57,7 @@ <h3><span class="glyphicon glyphicon-link"></span> Helpful Links</h3>
 </div>
 
 </body>
+<footer>
+<a href="https://icons8.com/icon/44790/cloud-storage">Cloud Storage icon by Icons8</a>
+</footer>
 </html>
diff --git a/src/main/resources/templates/onedrive.html b/src/main/resources/templates/onedrive.html
index 6a22eb6..e89ef22 100644
--- a/src/main/resources/templates/onedrive.html
+++ b/src/main/resources/templates/onedrive.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'index')}">
 <head>
-    <title>Onedrive OAuth Redirection</title>
+    <title>OAuth2 Redirection</title>
     <link rel="stylesheet" type="text/css" href="/stylesheets/main.css" />
 </head>
 
@@ -9,7 +9,7 @@
 
 <div class="container">
 
-<h1>Onedrive</h1>
+<h1>OAuth2 Error</h1>
 
 <h2>[[${error}]]</h2>
 
diff --git a/src/test/java/Test2.java b/src/test/java/Test2.java
index a309948..8a600e7 100644
--- a/src/test/java/Test2.java
+++ b/src/test/java/Test2.java
@@ -10,6 +10,7 @@
 import java.nio.file.Paths;
 import java.sql.Connection;
 import java.sql.DriverManager;
+import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
@@ -40,12 +41,14 @@ public class Test2 {
 
     public static void main(String[] args) throws Exception {
         Test2 app = new Test2();
-        PropsEntity.Util.bind(app);
+//        PropsEntity.Util.bind(app);
 System.err.println(app.url);
 //        app.drop();
 //        app.create();
 //        app.insert();
+//        app.update("box:your@id.com");
         app.select();
+//        app.select("box:your@id.com");
 
         System.err.println("done");
     }
@@ -66,7 +69,7 @@ void create() throws SQLException, IOException {
 
     void insert() throws SQLException, IOException {
         try (Connection connection = DriverManager.getConnection(url, user, password)) {
-            Statement stmt = connection.createStatement();
+            PreparedStatement pstmt = connection.prepareStatement("INSERT INTO credential VALUES (?, ?)");
 
             LocalStrageDao local = new LocalStrageDao("tmp/database.properties");
             for (String schemeId : local.load()) {
@@ -84,13 +87,23 @@ void insert() throws SQLException, IOException {
                     if (scheme.equals("msgraph")) {
                         scheme = "onedrive";
                     }
-                    stmt.executeUpdate("INSERT INTO credential VALUES ('" + scheme + ":" + email + "', '" + token + "')");
+                    pstmt.setString(1, scheme + ":" + email);
+                    pstmt.setString(2, token);
+                    pstmt.execute();
 System.err.println("DB: id: " + schemeId);
                 }
             }
         }
     }
 
+    void update(String id) throws SQLException, IOException {
+        try (Connection connection = DriverManager.getConnection(url, user, password)) {
+            PreparedStatement pstmt = connection.prepareStatement("UPDATE credential SET token = NULL WHERE id = ?");
+            pstmt.setString(1, id);
+            pstmt.execute();
+        }
+    }
+
     void select() throws SQLException {
         try (Connection connection = DriverManager.getConnection(url, user, password)) {
             Statement stmt = connection.createStatement();
@@ -101,6 +114,17 @@ void select() throws SQLException {
             }
         }
     }
+
+    void select(String id) throws SQLException {
+        try (Connection connection = DriverManager.getConnection(url, user, password)) {
+            PreparedStatement pstmt = connection.prepareStatement("SELECT token FROM credential WHERE id = ?");
+            pstmt.setString(1, id);
+            ResultSet rs = pstmt.executeQuery();
+            if (rs.next()) {
+System.err.println("DB: { id: " + id + ", token: " + rs.getString("token") + " }");
+            }
+        }
+    }
 }
 
 /* */