From 8dc715cf72119c4ed06b01955bf53b131932056d Mon Sep 17 00:00:00 2001 From: IT Hit Date: Wed, 19 Feb 2020 20:19:03 +0000 Subject: [PATCH] 4.3.3604 --- Java/androidfsstorage/app/build.gradle | 6 +- Java/deltav/pom.xml | 14 +- Java/filesystemstorage/pom.xml | 12 +- Java/oraclestorage/pom.xml | 14 +- Java/springboot/pom.xml | 89 +++- .../springboot/common/ResourceReader.java | 20 +- .../configuration/WebDavConfiguration.java | 43 +- .../WebDavConfigurationProperties.java | 2 +- .../springboot/controller/DavFilter.java | 3 - .../controller/SamplesController.java | 13 +- .../samples/springboot/impl/FileImpl.java | 28 ++ .../samples/springboot/impl/FolderImpl.java | 102 +++- .../springboot/impl/HierarchyItemImpl.java | 24 +- .../samples/springboot/impl/SearchFacade.java | 474 ++++++++++++++++++ .../samples/springboot/impl/WebDavEngine.java | 39 ++ .../springboot/websocket/SocketHandler.java | 29 ++ .../springboot/websocket/WebSocketServer.java | 77 +++ .../src/main/resources/application.properties | 5 +- .../handler/MyCustomHandlerPage.html | 1 + Kotlin/filesystemstorage/pom.xml | 12 +- 20 files changed, 932 insertions(+), 75 deletions(-) create mode 100644 Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/SearchFacade.java create mode 100644 Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/SocketHandler.java create mode 100644 Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/WebSocketServer.java diff --git a/Java/androidfsstorage/app/build.gradle b/Java/androidfsstorage/app/build.gradle index 38369f9..d224c33 100644 --- a/Java/androidfsstorage/app/build.gradle +++ b/Java/androidfsstorage/app/build.gradle @@ -30,7 +30,7 @@ android { jarJar { rules = [ 'stax-api-1.0.1.jar' : 'javax.xml.** com.ithit.webdav.xml.@1', - 'webdav-server-4.3.3576.jar': 'javax.xml.stream.** com.ithit.webdav.xml.stream.@1' + 'webdav-server-4.3.3605.jar': 'javax.xml.stream.** com.ithit.webdav.xml.stream.@1' ] } @@ -71,11 +71,11 @@ dependencies { implementation 'commons-io:commons-io:2.4' implementation 'com.google.code.gson:gson:2.7' implementation 'com.android.support:appcompat-v7:27.1.1' - implementation('com.ithit.webdav.integration:android-integration:4.3.3576', { + implementation('com.ithit.webdav.integration:android-integration:4.3.3605', { exclude group: 'org.nanohttpd', module: 'nanohttpd' }) implementation 'com.android.support.constraint:constraint-layout:1.0.2' jarJar 'stax:stax-api:1.0.1' - jarJar 'com.ithit.webdav:webdav-server:4.3.3576' + jarJar 'com.ithit.webdav:webdav-server:4.3.3605' testImplementation 'junit:junit:4.12' } \ No newline at end of file diff --git a/Java/deltav/pom.xml b/Java/deltav/pom.xml index 69524f2..fb70d8b 100644 --- a/Java/deltav/pom.xml +++ b/Java/deltav/pom.xml @@ -6,7 +6,7 @@ com.ithit.webdav.samples deltav - 4.3.3576 + 4.3.3604 war @@ -23,7 +23,7 @@ com.ithit.webdav.integration servlet-integration - 4.3.3576 + 4.3.3604 commons-dbcp @@ -61,12 +61,12 @@ org.apache.tika tika-core - 1.22 + 1.23 org.apache.tika tika-parsers - 1.22 + 1.23 cxf-core @@ -125,7 +125,7 @@ com.ithit.webdav webdav-server - 4.3.3576 + 4.3.3604 @@ -165,7 +165,7 @@ copy-resources - ${project.build.directory}/deltav-4.3.3576/META-INF + ${project.build.directory}/deltav-4.3.3604/META-INF true @@ -244,7 +244,7 @@ filesystem 11021 / - target/deltav-4.3.3576 + target/deltav-4.3.3604 diff --git a/Java/filesystemstorage/pom.xml b/Java/filesystemstorage/pom.xml index 56ef1f7..55c055c 100644 --- a/Java/filesystemstorage/pom.xml +++ b/Java/filesystemstorage/pom.xml @@ -6,7 +6,7 @@ com.ithit.webdav.samples filesystemstorage - 4.3.3576 + 4.3.3604 war @@ -34,7 +34,7 @@ com.ithit.webdav.integration servlet-integration - 4.3.3576 + 4.3.3604 commons-io @@ -78,12 +78,12 @@ org.apache.tika tika-core - 1.22 + 1.23 org.apache.tika tika-parsers - 1.22 + 1.23 cxf-core @@ -142,7 +142,7 @@ com.ithit.webdav webdav-server - 4.3.3576 + 4.3.3604 net.java.dev.jna @@ -228,7 +228,7 @@ filesystem 11021 / - target/filesystemstorage-4.3.3576 + target/filesystemstorage-4.3.3604 diff --git a/Java/oraclestorage/pom.xml b/Java/oraclestorage/pom.xml index b06634d..179434e 100644 --- a/Java/oraclestorage/pom.xml +++ b/Java/oraclestorage/pom.xml @@ -6,7 +6,7 @@ com.ithit.webdav.samples oraclestorage - 4.3.3576 + 4.3.3604 war @@ -23,7 +23,7 @@ com.ithit.webdav.integration servlet-integration - 4.3.3576 + 4.3.3604 commons-dbcp @@ -61,12 +61,12 @@ org.apache.tika tika-core - 1.22 + 1.23 org.apache.tika tika-parsers - 1.22 + 1.23 cxf-core @@ -125,7 +125,7 @@ com.ithit.webdav webdav-server - 4.3.3576 + 4.3.3604 @@ -165,7 +165,7 @@ copy-resources - ${project.build.directory}/oraclestorage-4.3.3576/META-INF + ${project.build.directory}/oraclestorage-4.3.3604/META-INF true @@ -244,7 +244,7 @@ filesystem 11021 / - target/oraclestorage-4.3.3576 + target/oraclestorage-4.3.3604 diff --git a/Java/springboot/pom.xml b/Java/springboot/pom.xml index fce52cb..49a15c3 100644 --- a/Java/springboot/pom.xml +++ b/Java/springboot/pom.xml @@ -9,12 +9,14 @@ com.ithit.webdav.samples springboot - 4.3.3576 + 4.3.3604 springboot Demo project for Spring Boot 1.8 + 7.5.0 + 1.23 @@ -52,12 +54,12 @@ com.ithit.webdav webdav-server - 4.3.3576 + 4.3.3604 com.ithit.webdav.integration servlet-integration - 4.3.3576 + 4.3.3604 @@ -81,6 +83,87 @@ 2.6 compile + + + + org.apache.lucene + lucene-core + ${lucene-core.version} + + + org.apache.lucene + lucene-queryparser + ${lucene-core.version} + + + org.apache.lucene + lucene-highlighter + ${lucene-core.version} + + + org.apache.tika + tika-core + ${tika-core.version} + + + org.apache.tika + tika-parsers + ${tika-core.version} + + + cxf-core + org.apache.cxf + + + cxf-rt-rs-client + org.apache.cxf + + + httpservices + edu.ucar + + + maven-scm-provider-svnexe + org.apache.maven.scm + + + maven-scm-api + org.apache.maven.scm + + + slf4j-log4j12 + org.slf4j + + + c3p0 + c3p0 + + + httpclient + org.apache.httpcomponents + + + grib + edu.ucar + + + cdm + edu.ucar + + + unit-api + javax.measure + + + activation + javax.activation + + + org.apache.sis.storage + sis-netcdf + + + diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/common/ResourceReader.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/common/ResourceReader.java index 4c6077e..6dbcb92 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/common/ResourceReader.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/common/ResourceReader.java @@ -1,11 +1,13 @@ package com.ithit.webdav.samples.springboot.common; import com.ithit.webdav.samples.springboot.SpringBootSampleApplication; +import com.ithit.webdav.samples.springboot.configuration.WebDavConfigurationProperties; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.boot.system.ApplicationHome; +import org.springframework.boot.system.ApplicationTemp; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternUtils; @@ -26,22 +28,27 @@ @Component public class ResourceReader { - public static String STORAGE_FOLDER = "Storage"; + public static String STORAGE_FOLDER = "StorageRoot"; + private static String INDEX_FOLDER = "Index"; private File root = new File(getRootFolder()); - private static String LOCAL_STATIC_RESOURCES_FOLDER_PATH = "classpath:" + STORAGE_FOLDER + "/**"; ResourceLoader resourceLoader; + WebDavConfigurationProperties properties; public void readFiles() { Resource[] resources; try { + final String storageRoot = "Storage"; + String LOCAL_STATIC_RESOURCES_FOLDER_PATH = "classpath:" + storageRoot + "/**"; resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) .getResources(LOCAL_STATIC_RESOURCES_FOLDER_PATH); for (Resource resource : resources) { if (resource.exists() & resource.isReadable()) { URL url = resource.getURL(); String urlString = url.toExternalForm(); - String targetName = urlString.substring(urlString.indexOf("Storage")); - File destination = new File(root, URLDecoder.decode(targetName, StandardCharsets.UTF_8.toString())); + String targetName = urlString.substring(urlString.indexOf(storageRoot)); + File destination = new File(root, Paths.get(STORAGE_FOLDER, + properties.getRootContext(), + URLDecoder.decode(targetName.substring(storageRoot.length()), StandardCharsets.UTF_8.toString())).toString()); if (!destination.exists()) { FileUtils.copyURLToFile(url, destination); } @@ -56,6 +63,11 @@ public String getRootFolder() { return home.getDir().getAbsolutePath(); } + public String getDefaultIndexFolder() { + ApplicationTemp temp = new ApplicationTemp(SpringBootSampleApplication.class); + return temp.getDir(INDEX_FOLDER).getAbsolutePath(); + } + public String getDefaultPath() { return Paths.get(getRootFolder(), STORAGE_FOLDER).toString(); } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfiguration.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfiguration.java index 6e01104..93b62ae 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfiguration.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfiguration.java @@ -3,7 +3,10 @@ import com.ithit.webdav.samples.springboot.common.ResourceReader; import com.ithit.webdav.samples.springboot.extendedattributes.ExtendedAttributesExtension; import com.ithit.webdav.samples.springboot.impl.CustomFolderGetHandler; +import com.ithit.webdav.samples.springboot.impl.SearchFacade; import com.ithit.webdav.samples.springboot.impl.WebDavEngine; +import com.ithit.webdav.samples.springboot.websocket.SocketHandler; +import com.ithit.webdav.samples.springboot.websocket.WebSocketServer; import com.ithit.webdav.server.Engine; import com.ithit.webdav.server.util.StringUtil; import lombok.AccessLevel; @@ -24,12 +27,16 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; @@ -38,8 +45,9 @@ @FieldDefaults(level = AccessLevel.PRIVATE) @EnableConfigurationProperties(WebDavConfigurationProperties.class) @EnableWebMvc +@EnableWebSocket @Configuration -public class WebDavConfiguration extends WebMvcConfigurerAdapter { +public class WebDavConfiguration extends WebMvcConfigurerAdapter implements WebSocketConfigurer { final WebDavConfigurationProperties properties; final ResourceReader resourceReader; private static String rootLocalPath = null; @@ -47,6 +55,7 @@ public class WebDavConfiguration extends WebMvcConfigurerAdapter { Resource customGetHandler; @Value("classpath:handler/attributesErrorPage.html") Resource errorPage; + private final SocketHandler socketHandler = new SocketHandler(); @Bean public CorsConfigurationSource corsConfigurationSource() { @@ -65,6 +74,11 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { .addResourceLocations("classpath:/wwwroot/", "/wwwroot/"); } + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(socketHandler, properties.getRootWebSocket()).setAllowedOrigins("*"); + } + @Bean public WebDavEngine engine() { rootLocalPath = properties.getRootFolder(); @@ -81,6 +95,13 @@ public WebDavEngine engine() { CustomFolderGetHandler handlerHead = new CustomFolderGetHandler(webDavEngine.getResponseCharacterEncoding(), Engine.getVersion(), extendedAttributesSupported, customGetHandler(), errorPage(), properties.getRootContext()); handler.setPreviousHandler(webDavEngine.registerMethodHandler("GET", handler)); handlerHead.setPreviousHandler(webDavEngine.registerMethodHandler("HEAD", handlerHead)); + String indexLocalPath = createIndexPath(); + if (rootLocalPath != null && indexLocalPath != null) { + SearchFacade searchFacade = new SearchFacade(webDavEngine, webDavEngine.getLogger()); + searchFacade.indexRootFolder(rootLocalPath, indexLocalPath, 2); + webDavEngine.setSearchFacade(searchFacade); + } + webDavEngine.setWebSocketServer(new WebSocketServer(socketHandler.getSessions())); return webDavEngine; } @@ -113,8 +134,7 @@ private void checkRootPath(String rootPath) { if (Files.exists(Paths.get(realPath, rootPath))) { rootLocalPath = Paths.get(realPath, rootPath).toString(); } else { - resourceReader.readFiles(); - rootLocalPath = resourceReader.getDefaultPath(); + createDefaultPath(); } } catch (Exception ignored) { createDefaultPath(); @@ -126,4 +146,21 @@ private void createDefaultPath() { resourceReader.readFiles(); rootLocalPath = resourceReader.getDefaultPath(); } + + /** + * Creates index folder if not exists. + * + * @return Absolute location of index folder. + */ + private String createIndexPath() { + Path indexLocalPath = Paths.get(resourceReader.getDefaultIndexFolder()); + if (Files.notExists(indexLocalPath)) { + try { + Files.createDirectory(indexLocalPath); + } catch (IOException e) { + return null; + } + } + return indexLocalPath.toString(); + } } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfigurationProperties.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfigurationProperties.java index 54083fe..ba6b763 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfigurationProperties.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/configuration/WebDavConfigurationProperties.java @@ -11,9 +11,9 @@ @FieldDefaults(level = AccessLevel.PRIVATE) @ConfigurationProperties(prefix = "webdav") public class WebDavConfigurationProperties { - public static final String ROOT_ATTRIBUTE = "originalRequest"; String license; boolean showExceptions; String rootFolder; String rootContext; + String rootWebSocket; } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/DavFilter.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/DavFilter.java index d601382..aee9138 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/DavFilter.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/DavFilter.java @@ -11,8 +11,6 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; -import static com.ithit.webdav.samples.springboot.configuration.WebDavConfigurationProperties.ROOT_ATTRIBUTE; - @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @@ -32,7 +30,6 @@ public void doFilter( if ((req.getMethod().equalsIgnoreCase("PROPFIND") || req.getMethod().equalsIgnoreCase("OPTIONS")) && properties.getRootContext().contains(req.getRequestURI()) && properties.getRootContext().length() - 2 > req.getRequestURI().length()) { - request.setAttribute(ROOT_ATTRIBUTE, req.getRequestURI()); request.getRequestDispatcher(properties.getRootContext()).include(request, response); } else { chain.doFilter(request, response); diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/SamplesController.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/SamplesController.java index d6c57d3..ac447b6 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/SamplesController.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/controller/SamplesController.java @@ -2,7 +2,6 @@ import com.ithit.webdav.integration.servlet.HttpServletDavRequest; import com.ithit.webdav.integration.servlet.HttpServletDavResponse; -import com.ithit.webdav.samples.springboot.configuration.WebDavConfigurationProperties; import com.ithit.webdav.samples.springboot.impl.WebDavEngine; import com.ithit.webdav.server.exceptions.DavException; import com.ithit.webdav.server.exceptions.WebDavStatus; @@ -19,8 +18,6 @@ import java.io.IOException; import java.io.PrintStream; -import static com.ithit.webdav.samples.springboot.configuration.WebDavConfigurationProperties.ROOT_ATTRIBUTE; - @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @@ -28,7 +25,6 @@ public class SamplesController { WebDavEngine engine; - WebDavConfigurationProperties properties; @RequestMapping(path = "${webdav.rootContext}**", produces = MediaType.ALL_VALUE) public void webdav(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException { @@ -41,17 +37,10 @@ public void options(HttpServletRequest httpServletRequest, HttpServletResponse h } private void performDavRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException { - final Object originalRequest = httpServletRequest.getAttribute(ROOT_ATTRIBUTE); - boolean isRoot = originalRequest != null; HttpServletDavRequest davRequest = new HttpServletDavRequest(httpServletRequest) { @Override public String getServerPath() { - return isRoot ? originalRequest.toString() : properties.getRootContext(); - } - - @Override - public String getRequestURI() { - return isRoot ? originalRequest.toString() : super.getRequestURI(); + return "/"; } }; HttpServletDavResponse davResponse = new HttpServletDavResponse(httpServletResponse); diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FileImpl.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FileImpl.java index f81e92c..d85c5af 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FileImpl.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FileImpl.java @@ -275,11 +275,18 @@ public long write(InputStream content, String contentType, long startIndex, long writer.write(byteBuffer); totalWrittenBytes += readBytes; } + + try { + getEngine().getSearchFacade().getIndexer().indexFile(getName(), decode(getPath()), null, this); + } catch (Exception ex){ + getEngine().getLogger().logError("Errors during indexing.", ex); + } } catch (Exception ex) { ex.printStackTrace(); } finally { writer.close(); } + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); return totalWrittenBytes; } // writeFileImpl >>>> @@ -316,6 +323,12 @@ public void delete() throws LockedException, MultistatusException, ServerExcepti getEngine().getLogger().logError("Tried to delete file in use.", e); throw new ServerException(e); } + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); + try { + getEngine().getSearchFacade().getIndexer().deleteIndex(this); + } catch (Exception ex) { + getEngine().getLogger().logError("Errors during indexing.", ex); + } } @Override @@ -336,6 +349,13 @@ public void copyTo(Folder folder, String destName, boolean deep) if (ExtendedAttributesExtension.hasExtendedAttribute(newPath.toString(), activeLocksAttribute)) { ExtendedAttributesExtension.deleteExtendedAttribute(newPath.toString(), activeLocksAttribute); } + getEngine().getWebSocketServer().notifyRefresh(folder.getPath()); + try { + String currentPath = folder.getPath() + destName; + getEngine().getSearchFacade().getIndexer().indexFile(decode(destName), decode(currentPath), null, this); + } catch (Exception ex) { + getEngine().getLogger().logError("Errors during indexing.", ex); + } } @Override @@ -358,6 +378,14 @@ public void moveTo(Folder folder, String destName) throws LockedException, if (ExtendedAttributesExtension.hasExtendedAttribute(newPath.toString(), activeLocksAttribute)) { ExtendedAttributesExtension.deleteExtendedAttribute(newPath.toString(), activeLocksAttribute); } + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); + getEngine().getWebSocketServer().notifyRefresh(folder.getPath()); + try { + String currentPath = folder.getPath() + destName; + getEngine().getSearchFacade().getIndexer().indexFile(decode(destName), decode(currentPath), getPath(), this); + } catch (Exception ex) { + getEngine().getLogger().logError("Errors during indexing.", ex); + } } /** diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FolderImpl.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FolderImpl.java index 3ac5ce4..c1fc3cc 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FolderImpl.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/FolderImpl.java @@ -2,6 +2,7 @@ import com.ithit.webdav.server.File; import com.ithit.webdav.server.Folder; +import com.ithit.webdav.server.HierarchyItem; import com.ithit.webdav.server.Property; import com.ithit.webdav.server.exceptions.ConflictException; import com.ithit.webdav.server.exceptions.LockedException; @@ -11,6 +12,8 @@ import com.ithit.webdav.server.paging.PageResults; import com.ithit.webdav.server.quota.Quota; import com.ithit.webdav.server.resumableupload.ResumableUploadBase; +import com.ithit.webdav.server.search.Search; +import com.ithit.webdav.server.search.SearchOptions; import org.apache.commons.io.FileUtils; import java.io.IOException; @@ -20,10 +23,7 @@ import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -31,7 +31,7 @@ /** * Represents a folder in the File system repository. */ -class FolderImpl extends HierarchyItemImpl implements Folder, Quota, ResumableUploadBase { +class FolderImpl extends HierarchyItemImpl implements Folder, Search, Quota, ResumableUploadBase { /** @@ -96,7 +96,6 @@ private static String fixPath(String path) { * @throws LockedException This folder was locked. Client did not provide the lock token. * @throws ServerException In case of an error. */ - // <<<< createFileImpl @Override public FileImpl createFile(String name) throws LockedException, ServerException { ensureHasToken(); @@ -108,11 +107,11 @@ public FileImpl createFile(String name) throws LockedException, ServerException } catch (IOException e) { throw new ServerException(e); } + getEngine().getWebSocketServer().notifyRefresh(getPath()); return FileImpl.getFile(getPath() + encode(name), getEngine()); } return null; } - // createFileImpl >>>> /** * Creates new {@link FolderImpl} folder with the specified name in this folder. @@ -121,7 +120,6 @@ public FileImpl createFile(String name) throws LockedException, ServerException * @throws LockedException This folder was locked. Client did not provide the lock token. * @throws ServerException In case of an error. */ - // <<<< createFolderImpl @Override public void createFolder(String name) throws LockedException, ServerException { @@ -131,12 +129,12 @@ public void createFolder(String name) throws LockedException, if (!Files.exists(fullPath)) { try { Files.createDirectory(fullPath); + getEngine().getWebSocketServer().notifyRefresh(getPath()); } catch (IOException e) { throw new ServerException(e); } } } - // createFolderImpl >>>> /** * Gets the array of this folder's children. @@ -148,7 +146,6 @@ public void createFolder(String name) throws LockedException, * @return Instance of {@link PageResults} class that contains items on a requested page and total number of items in a folder. * @throws ServerException In case of an error. */ - // <<<< getChildren @Override public PageResults getChildren(List propNames, Long offset, Long nResults, List orderProps) throws ServerException { String decodedPath = HierarchyItemImpl.decodeAndConvertToPath(getPath()); @@ -173,22 +170,20 @@ public PageResults getChildren(List propNames, Long offset, Long nResu } return new PageResults(children, total); } - // getChildren >>>> - // <<<< deleteFolderImpl @Override public void delete() throws LockedException, ServerException { ensureHasToken(); try { + removeIndex(getFullPath(), this); FileUtils.deleteDirectory(getFullPath().toFile()); + getEngine().getWebSocketServer().notifyDelete(getPath()); } catch (IOException e) { throw new ServerException(e); } } - // deleteFolderImpl >>>> - // <<<< copyToFolderImpl @Override public void copyTo(Folder folder, String destName, boolean deep) throws LockedException, ServerException { @@ -205,12 +200,13 @@ public void copyTo(Folder folder, String destName, boolean deep) Path sourcePath = this.getFullPath(); Path destinationFullPath = Paths.get(destinationFolder, destName); FileUtils.copyDirectory(sourcePath.toFile(), destinationFullPath.toFile()); + getEngine().getWebSocketServer().notifyRefresh(folder.getPath()); + addIndex(destinationFullPath, folder.getPath() + destName, destName); } catch (IOException e) { throw new ServerException(e); } setName(destName); } - // copyToFolderImpl >>>> /** * Check whether current folder is the parent to the destination. @@ -285,7 +281,6 @@ private String getExtension(String name) { } - // <<<< moveToFolderImpl @Override public void moveTo(Folder folder, String destName) throws LockedException, ConflictException, ServerException { @@ -299,12 +294,59 @@ public void moveTo(Folder folder, String destName) throws LockedException, try { FileUtils.copyDirectory(sourcePath.toFile(), destinationFullPath.toFile()); delete(); + addIndex(destinationFullPath, folder.getPath() + destName, destName); } catch (IOException e) { throw new ServerException(e); } setName(destName); + getEngine().getWebSocketServer().notifyDelete(getPath()); + getEngine().getWebSocketServer().notifyRefresh(folder.getPath()); + } + + /** + * Returns list of items that correspond to search request. + * + * @param searchString A phrase to search. + * @param options Search parameters. + * @param propNames List of properties to retrieve with the children. They will be queried by the engine later. + * @param offset The number of items to skip before returning the remaining items. + * @param nResults The number of items to return. + * @return Instance of {@link PageResults} class that contains items on a requested page and total number of items in search results. + */ + // <<<< searchImpl + @Override + public PageResults search(String searchString, SearchOptions options, List propNames, Long offset, Long nResults) { + List results = new LinkedList<>(); + SearchFacade.Searcher searcher = getEngine().getSearchFacade().getSearcher(); + if (searcher == null) { + return new PageResults(results, (long) results.size()); + } + boolean snippet = false; + if (propNames.stream().filter(x -> SNIPPET.equalsIgnoreCase(x.getName())).findFirst().orElse(null) != null) { + snippet = true; + } + Map searchResult; + try { + String decodedPath = decode(getPath()); + searchResult = searcher.search(searchString, options, decodedPath, snippet); + for (Map.Entry entry : searchResult.entrySet()) { + try { + HierarchyItem item = getEngine().getHierarchyItem(entry.getKey()); + if (item != null) { + if (snippet && item instanceof FileImpl) { + ((FileImpl) item).setSnippet(entry.getValue()); + } + results.add(item); + } + } catch (Exception ex) { + getEngine().getLogger().logError("Error during search.", ex); + } + } + } catch (ServerException e) { + getEngine().getLogger().logError("Error during search.", e); + } + return new PageResults((offset != null && nResults != null) ? results.stream().skip(offset).limit(nResults).collect(Collectors.toList()) : results, (long) results.size()); } - // moveToFolderImpl >>>> /** * Returns free bytes available to current user. @@ -326,4 +368,30 @@ public long getUsedBytes() { long total = getFullPath().toFile().getTotalSpace(); return total - getAvailableBytes(); } + + private void removeIndex(Path sourcePath, FolderImpl itSelf) { + List filesToDelete = new ArrayList<>(); + getEngine().getSearchFacade().getFilesToIndex(sourcePath.toFile().listFiles(), filesToDelete, getRootFolder()); + filesToDelete.add(itSelf); + for (HierarchyItem hi : filesToDelete) { + try { + getEngine().getSearchFacade().getIndexer().deleteIndex(hi); + } catch (Exception e) { + getEngine().getLogger().logError("Cannot delete index.", e); + } + } + } + + private void addIndex(Path sourcePath, String path, String name) { + List filesToIndex = new ArrayList<>(); + getEngine().getSearchFacade().getFilesToIndex(sourcePath.toFile().listFiles(), filesToIndex, getRootFolder()); + getEngine().getSearchFacade().getIndexer().indexFile(name, decode(path), null, null); + for (HierarchyItem hi : filesToIndex) { + try { + getEngine().getSearchFacade().getIndexer().indexFile(hi.getName(), decode(hi.getPath()), null, hi); + } catch (Exception e) { + getEngine().getLogger().logError("Cannot index.", e); + } + } + } } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/HierarchyItemImpl.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/HierarchyItemImpl.java index 07626ad..02bf90f 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/HierarchyItemImpl.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/HierarchyItemImpl.java @@ -3,6 +3,7 @@ import com.ithit.webdav.samples.springboot.extendedattributes.ExtendedAttributesExtension; import com.ithit.webdav.server.*; import com.ithit.webdav.server.exceptions.*; +import com.ithit.webdav.server.util.StringUtil; import java.io.File; import java.io.UnsupportedEncodingException; @@ -201,7 +202,7 @@ void setName(String name) { * @return Item path relative to storage root. */ @Override - public String getPath() { + public String getPath() throws ServerException { return path; } @@ -323,6 +324,7 @@ public void updateProperties(Property[] setProps, Property[] delProps) .filter(e -> !propNamesToDel.contains(e.getName())) .collect(Collectors.toList()); ExtendedAttributesExtension.setExtendedAttribute(getFullPath().toString(), propertiesAttribute, SerializationUtils.serialize(properties)); + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); } // updatePropertiesImpl >>>> @@ -371,7 +373,11 @@ WebDavEngine getEngine() { * @return Full path in the File System to the {@link HierarchyItemImpl}. */ Path getFullPath() { - String fullPath = getRootFolder() + HierarchyItemImpl.decodeAndConvertToPath(getPath()); + String fullPath = ""; + try { + fullPath = getRootFolder() + HierarchyItemImpl.decodeAndConvertToPath(getPath()); + } catch (ServerException ignored) { + } return Paths.get(fullPath); } @@ -403,6 +409,7 @@ public LockResult lock(boolean shared, boolean deep, long timeout, String owner) LockInfo lockInfo = new LockInfo(shared, deep, token, expires, owner); activeLocks.add(lockInfo); ExtendedAttributesExtension.setExtendedAttribute(getFullPath().toString(), activeLocksAttribute, SerializationUtils.serialize(activeLocks)); + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); return new LockResult(token, timeout); } // lockImpl >>>> @@ -459,6 +466,7 @@ public void unlock(String lockToken) throws PreconditionFailedException, } else { ExtendedAttributesExtension.deleteExtendedAttribute(getFullPath().toString(), activeLocksAttribute); } + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); } else { throw new PreconditionFailedException(); } @@ -491,8 +499,20 @@ public RefreshLockResult refreshLock(String token, long timeout) long expires = System.currentTimeMillis() + timeout * 1000; lockInfo.setTimeout(expires); ExtendedAttributesExtension.setExtendedAttribute(getFullPath().toString(), activeLocksAttribute, SerializationUtils.serialize(activeLocks)); + getEngine().getWebSocketServer().notifyRefresh(getParent(getPath())); return new RefreshLockResult(lockInfo.isShared(), lockInfo.isDeep(), timeout, lockInfo.getOwner()); } // refreshLockImpl >>>> + + String getParent(String path) { + String parentPath = StringUtil.trimEnd(StringUtil.trimStart(path, "/"), "/"); + int index = parentPath.lastIndexOf("/"); + if (index > -1) { + parentPath = parentPath.substring(0, index); + } else { + parentPath = ""; + } + return parentPath; + } } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/SearchFacade.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/SearchFacade.java new file mode 100644 index 0000000..b33cb3e --- /dev/null +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/SearchFacade.java @@ -0,0 +1,474 @@ +package com.ithit.webdav.samples.springboot.impl; + +import com.ithit.webdav.server.HierarchyItem; +import com.ithit.webdav.server.Logger; +import com.ithit.webdav.server.search.SearchOptions; +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.*; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.*; +import org.apache.lucene.search.highlight.*; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.tika.Tika; +import org.apache.tika.exception.ZeroByteFileException; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.RecursiveAction; +import java.util.regex.Pattern; + +/** + * Facade that encapsulates all functionality regarding indexing and searching + */ +public class SearchFacade { + private Indexer indexer; + private Searcher searcher; + private WebDavEngine engine; + private Logger logger; + + public SearchFacade(WebDavEngine webDavEngine, Logger logger) { + engine = webDavEngine; + this.logger = logger; + } + + /** + * Returns Indexer instance + * + * @return Indexer instance + */ + Indexer getIndexer() { + return indexer; + } + + /** + * Returns Searcher instance + * + * @return Searcher instance + */ + Searcher getSearcher() { + return searcher; + } + + /** + * This task is running in background and indexes all folder mapped for WebDav asynchronously. + * So there is a chance in case of big folder there will be some delays while this task + * will finish indexation. + */ + private class IndexTask extends TimerTask { + + private String dataFolder; + private String indexFolder; + private Integer interval; + + /** + * Build initial index of root folder. + * + * @param dataFolder Root folder. + * @param indexFolder Index folder. + * @param interval Daemon commit interval. + */ + IndexTask(String dataFolder, String indexFolder, Integer interval) { + this.dataFolder = dataFolder; + this.indexFolder = indexFolder; + this.interval = interval; + } + + /** + * The action to be performed by this timer task. + */ + @Override + public void run() { + ForkJoinPool forkJoinPool = new ForkJoinPool(4); + Directory fsDir; + try { + List filesToIndex = new ArrayList<>(); + File data = new File(dataFolder); + StandardAnalyzer standardAnalyzer = new StandardAnalyzer(); + searcher = new Searcher(indexFolder, standardAnalyzer, logger); + getFilesToIndex(data.listFiles(), filesToIndex, dataFolder); + fsDir = FSDirectory.open(Paths.get(indexFolder)); + IndexWriterConfig conf = new IndexWriterConfig(standardAnalyzer); + conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); + IndexWriter indexWriter = new IndexWriter(fsDir, conf); + Tika tika = new Tika(); + tika.setMaxStringLength(Indexer.MAX_CONTENT_LENGTH); + indexer = new Indexer(indexWriter, filesToIndex, logger, tika, dataFolder); + forkJoinPool.execute(indexer); + indexWriter.commit(); + new Indexer.CommitTask(indexWriter, logger).schedule(interval); + } catch (Throwable e) { + logger.logError("Cannot initialize Lucene", e); + } + } + + void schedule() { + Timer timer = new Timer(true); + timer.schedule(this, 0); + } + } + + /** + * Build initial index of root folder. + * + * @param dataFolder Root folder. + * @param indexFolder Index folder. + * @param interval Daemon commit interval. + */ + public void indexRootFolder(String dataFolder, String indexFolder, Integer interval) { + new IndexTask(dataFolder, indexFolder, interval).schedule(); + } + + /** + * Builds list of the all files (including files in child folders) in the data folder. + * + * @param files List of files in root folder. + * @param result List to be populated with results. + * @param dataFolder Root folder absolute location. + */ + void getFilesToIndex(File[] files, List result, String dataFolder) { + for (File f : files) { + if (f.isDirectory() && f.canRead() && !f.isHidden()) { + addFileToTheList(result, dataFolder, f); + getFilesToIndex(f.listFiles(), result, dataFolder); + } else { + if (f.canRead() && !f.isHidden()) { + addFileToTheList(result, dataFolder, f); + } + } + } + } + + private void addFileToTheList(List result, String dataFolder, File f) { + String quote = Pattern.quote(dataFolder); + try { + String context = f.getAbsolutePath().replaceAll("(?i)" + quote, ""); + context = context.replaceAll("\\\\", "/"); + if (f.isDirectory()) { + context += "/"; + } + result.add(engine.getHierarchyItem(context)); + } catch (Throwable e) { + logger.logDebug("Cannot add a file to the list: " + f.getAbsolutePath()); + } + } + + + /** + * Index files in storage using Apache Lucene engine for indexing and Apache Tika. + */ + static class Indexer extends RecursiveAction { + static final int MAX_CONTENT_LENGTH = 10 * 1024 * 1024; + private static final int TASK_INTERVAL = 30 * 1000; + static final String PATH = "path"; + static final String NAME = "name"; + static final String PARENT_NAME = "parent_name"; + static final String CONTENTS = "contents"; + private IndexWriter indexWriter; + private List files; + private Logger logger; + private Tika tika; + private String dataRoot; + private final static int BATCH_SIZE = 100; + + /** + * Create an instance of Indexer file. + * + * @param iw {@link IndexWriter} Lucene index writer. + * @param files List of the file to index. + * @param logger {@link Logger}. + * @param tika {@link Tika} to read content. + * @param dataRoot Files location root folder. + */ + private Indexer(IndexWriter iw, List files, Logger logger, Tika tika, String dataRoot) { + this.indexWriter = iw; + this.files = files; + this.logger = logger; + this.tika = tika; + this.dataRoot = dataRoot; + } + + @Override + protected void compute() { + if (files.size() > BATCH_SIZE) { + List tasks = new ArrayList<>(); + List> partitioned = chopped(files, BATCH_SIZE); + for (List sublist : partitioned) { + tasks.add(new Indexer(indexWriter, sublist, logger, tika, dataRoot)); + } + invokeAll(tasks); + } else { + for (HierarchyItem f : files) { + try { + indexFile(f.getName(), f.getPath(), null, f); + } catch (Throwable e) { + logger.logDebug("Cannot find path for this file."); + } + } + } + } + + private static List> chopped(List list, final int L) { + List> parts = new ArrayList<>(); + final int N = list.size(); + for (int i = 0; i < N; i += L) { + parts.add(new ArrayList<>( + list.subList(i, Math.min(N, i + L))) + ); + } + return parts; + } + + /** + * Indexes file. + * + * @param fileName File name to add to index. + * @param currentPath Current relative path of the file. + * @param oldPath Old relative path of the file if it was moved. + */ + void indexFile(String fileName, String currentPath, String oldPath, HierarchyItem item) { + Path fullPath = Paths.get(dataRoot, currentPath); + try { + Metadata metadata = new Metadata(); + Document doc = new Document(); + String parentFolder = currentPath.replace(fileName, "").replaceAll("/", ""); + Field pathField = new StringField(PATH, currentPath, Field.Store.YES); + Field parentField = new TextField(PARENT_NAME, parentFolder, Field.Store.YES); + Field nameField = new TextField(NAME, fileName, Field.Store.YES); + doc.add(pathField); + doc.add(parentField); + doc.add(nameField); + if (item instanceof FileImpl) { + try (TikaInputStream stream = TikaInputStream.get(fullPath, metadata)) { + String content = tika.parseToString(stream, metadata, MAX_CONTENT_LENGTH); + doc.add(new TextField(CONTENTS, content, Field.Store.YES)); + } catch (Throwable e) { + if (!(e instanceof ZeroByteFileException)) { + logger.logError("Error while indexing content: " + fullPath, e); + } + } + } + if (indexWriter.getConfig().getOpenMode() == IndexWriterConfig.OpenMode.CREATE) { + indexWriter.addDocument(doc); + } else { + indexWriter.updateDocument(new Term(PATH, oldPath != null ? oldPath : currentPath), doc); + } + } catch (Throwable e) { + logger.logError("Error while indexing file: " + fullPath, e); + } + } + + /** + * Close index and release lock + */ + void stop() { + try { + indexWriter.close(); + } catch (Throwable e) { + logger.logError("Cannot release index resources", e); + } + } + + /** + * Deletes specified file information from the index. + * + * @param file {@link FileImpl} to delete from index. + */ + void deleteIndex(HierarchyItem file) { + try { + indexWriter.deleteDocuments(new Term(PATH, file.getPath())); + } catch (Throwable e) { + logger.logError("Cannot delete index for the file.", e); + } + } + + /** + * Timer task implementation to commit index changes from time to time. + */ + static class CommitTask extends TimerTask { + + private IndexWriter indexWriter; + private Logger logger; + + /** + * Creates instance of {@link CommitTask}. + * + * @param indexWriter {@link IndexWriter} Lucene index writer. + * @param logger {@link Logger}. + */ + CommitTask(IndexWriter indexWriter, Logger logger) { + this.indexWriter = indexWriter; + this.logger = logger; + } + + /** + * The action to be performed by this timer task. + */ + @Override + public void run() { + try { + indexWriter.commit(); + } catch (Throwable e) { + logger.logError("Cannot commit.", e); + } + } + + /** + * Schedule timer executions at the specified Interval. + * + * @param interval Timer interval. + */ + void schedule(Integer interval) { + Timer timer = new Timer(true); + timer.scheduleAtFixedRate(this, 0, interval == null ? TASK_INTERVAL : interval * 1000); + } + } + } + + /** + * Search files information in Lucene index. + */ + static class Searcher { + + private String indexFolder; + private QueryParser nameParser; + private QueryParser contentParser; + private QueryParser parentParser; + private Logger logger; + private IndexSearcher indexSearcher; + + /** + * Creates instance of {@link Searcher}. + * + * @param indexFolder Index folder absolute location. + * @param standardAnalyzer Lucene {@link StandardAnalyzer}. + * @param logger {@link Logger}. + */ + private Searcher(String indexFolder, StandardAnalyzer standardAnalyzer, Logger logger) { + this.indexFolder = indexFolder; + nameParser = new QueryParser(Indexer.NAME, standardAnalyzer); + nameParser.setAllowLeadingWildcard(true); + contentParser = new QueryParser(Indexer.CONTENTS, standardAnalyzer); + contentParser.setAllowLeadingWildcard(true); + parentParser = new QueryParser(Indexer.PARENT_NAME, standardAnalyzer); + parentParser.setAllowLeadingWildcard(true); + this.logger = logger; + } + + /** + * Searches files by search line either in file name or in content. + * + * Ajax File Browser accepts regular wild cards used in most OS: + * + * ‘*’ – to indicate one or more character. + * ‘?’ – to indicate exactly one character. + * The ‘*’ and ‘?’ characters are replaced with ‘%’ and ‘_’ characters to comply with DASL standard when submitted to the server. + * + * If ‘%’, ‘_’ or ‘\’ characters are used in search phrase they are escaped with ‘\%’, ‘\_’ and ‘\\’. + * + * To make the search behave similarly to how file system search functions Ajax File Browser + * will automatically add ‘%’ character at the end of the search phrase. To search for the exact match wrap the search phrase in double quotes: “my file”. + * + * @param searchLine Line to search. + * @param options {@link SearchOptions} indicates where to search. + * @param parent Folder location in which to search. + * @return Map of paths to found items. + */ + Map search(String searchLine, SearchOptions options, String parent, boolean snippet) { + searchLine = StringEscapeUtils.escapeJava(searchLine); + searchLine = searchLine.replaceAll("%", "*"); + searchLine = searchLine.replaceAll("_", "?"); + Map paths = new LinkedHashMap<>(); + try (IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(indexFolder)))) { + indexSearcher = new IndexSearcher(reader); + if (options.isSearchContent()) { + paths.putAll(searchContent(searchLine, parent, snippet, reader)); + } + if (options.isSearchName()) { + paths.putAll(searchName(searchLine, parent)); + } + } catch (Throwable e) { + logger.logError("Error while doing index search.", e); + } + return paths; + } + + // Searches files by search line in file name + private Map searchName(String searchLine, String parent) throws Exception { + Query query = nameParser.parse(searchLine); + BooleanQuery.Builder finalQuery = addParentQuery(parent, query); + return search(finalQuery.build()); + } + + // Searches files by search line in file content + private Map searchContent(String searchLine, String parent, boolean withSnippet, IndexReader indexReader) throws Exception { + Query query = contentParser.parse(searchLine); + BooleanQuery.Builder finalQuery = addParentQuery(parent, query); + BooleanQuery booleanQuery = finalQuery.build(); + if (withSnippet) { + return searchWithSnippet(indexReader, booleanQuery); + } + return search(booleanQuery); + } + + // Searches files by search line in file name and adds highlights for found words + private Map searchWithSnippet(IndexReader indexReader, Query query) throws Exception { + QueryScorer queryScorer = new QueryScorer(query, Indexer.CONTENTS); + Fragmenter fragmenter = new SimpleSpanFragmenter(queryScorer); + SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter(); + Highlighter highlighter = new Highlighter(htmlFormatter, queryScorer); + highlighter.setTextFragmenter(fragmenter); + + ScoreDoc[] scoreDocs = indexSearcher.search(query, 100).scoreDocs; + Map result = new LinkedHashMap<>(); + for (ScoreDoc scoreDoc : scoreDocs) { + Document document = indexSearcher.doc(scoreDoc.doc); + String text = document.get(Indexer.CONTENTS); + String path = document.get(Indexer.PATH); + TokenStream tokenStream = TokenSources.getAnyTokenStream(indexReader, + scoreDoc.doc, Indexer.CONTENTS, document, new StandardAnalyzer()); + String fragment = highlighter.getBestFragment(tokenStream, text); + result.put(path, fragment == null ? "" : fragment); + } + return result; + } + + // Adds parent folder to the query to make search only in this folder + private BooleanQuery.Builder addParentQuery(String parent, Query query) throws ParseException { + BooleanQuery.Builder finalQuery = new BooleanQuery.Builder(); + finalQuery.add(query, BooleanClause.Occur.MUST); // MUST implies that the keyword must occur. + String searchString = parent.replaceAll("/", "") + "*"; + if (!Objects.equals(parent, "/")) { + Query parentQuery = parentParser.parse(searchString); + finalQuery.add(parentQuery, BooleanClause.Occur.MUST); + } + return finalQuery; + } + + // Searches files by search query either in file name or in content. + private Map search(Query query) throws IOException { + TopDocs search = indexSearcher.search(query, 100); + ScoreDoc[] hits = search.scoreDocs; + Map paths = new LinkedHashMap<>(); + for (ScoreDoc hit : hits) { + Document doc = indexSearcher.doc(hit.doc); + String path = doc.get(Indexer.PATH); + paths.put(path, ""); + } + return paths; + } + } +} diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/WebDavEngine.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/WebDavEngine.java index 8acdf20..d4fcee5 100644 --- a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/WebDavEngine.java +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/impl/WebDavEngine.java @@ -1,6 +1,7 @@ package com.ithit.webdav.samples.springboot.impl; import com.ithit.webdav.integration.servlet.HttpServletDavRequest; +import com.ithit.webdav.samples.springboot.websocket.WebSocketServer; import com.ithit.webdav.server.Engine; import com.ithit.webdav.server.HierarchyItem; import com.ithit.webdav.server.Logger; @@ -20,6 +21,8 @@ public class WebDavEngine extends Engine { private HttpServletDavRequest request; static String dataFolder; private boolean showExceptions; + private SearchFacade searchFacade; + private WebSocketServer webSocketServer; /** * Initializes a new instance of the WebDavEngine class. @@ -107,4 +110,40 @@ public void setServletRequest(HttpServletDavRequest httpServletRequest) { public boolean isShowExceptions() { return showExceptions; } + + /** + * Returns SearchFacade instance + * + * @return SearchFacade instance + */ + SearchFacade getSearchFacade() { + return searchFacade; + } + + /** + * Sets SearchFacade instance + * + * @param searchFacade SearchFacade instance + */ + public void setSearchFacade(SearchFacade searchFacade) { + this.searchFacade = searchFacade; + } + + /** + * Sets web socket server instance + * + * @param webSocketServer web socket server instance + */ + public void setWebSocketServer(WebSocketServer webSocketServer) { + this.webSocketServer = webSocketServer; + } + + /** + * Returns web socket server instance + * + * @return web socket server instance + */ + WebSocketServer getWebSocketServer() { + return webSocketServer; + } } diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/SocketHandler.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/SocketHandler.java new file mode 100644 index 0000000..ff94fe6 --- /dev/null +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/SocketHandler.java @@ -0,0 +1,29 @@ +package com.ithit.webdav.samples.springboot.websocket; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class SocketHandler extends TextWebSocketHandler { + + private final List sessions = new CopyOnWriteArrayList<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + sessions.add(session); + super.afterConnectionEstablished(session); + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + sessions.remove(session); + super.afterConnectionClosed(session, status); + } + + public List getSessions() { + return sessions; + } +} diff --git a/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/WebSocketServer.java b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/WebSocketServer.java new file mode 100644 index 0000000..66dc736 --- /dev/null +++ b/Java/springboot/src/main/java/com/ithit/webdav/samples/springboot/websocket/WebSocketServer.java @@ -0,0 +1,77 @@ +package com.ithit.webdav.samples.springboot.websocket; + +import com.ithit.webdav.server.util.StringUtil; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; +import java.util.List; + +/** + * WebSocket server, creates web socket endpoint, handles client's sessions + */ +public class WebSocketServer { + + private List sessions; + + public WebSocketServer(List sessions) { + this.sessions = sessions; + } + + /** + * Send notification to the client + * + * @param type of the notification + * @param folder to notify + */ + private void send(String type, String folder) { + for (WebSocketSession session: sessions) { + try { + session.sendMessage(new TextMessage(new Notification(folder, type).toString())); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * Sends refresh notification to the web socket client + * + * @param folder to refresh + */ + public void notifyRefresh(String folder) { + folder = StringUtil.trimEnd(StringUtil.trimStart(folder, "/"), "/"); + send("refresh", folder); + } + + /** + * Sends delete notification to the web socket client + * + * @param folder to delete + */ + public void notifyDelete(String folder) { + folder = StringUtil.trimEnd(StringUtil.trimStart(folder, "/"), "/"); + send("delete", folder); + } + + /** + * Represents VO to exchange between client and server + */ + static class Notification { + private String folderPath; + private String eventType; + + Notification(String folderPath, String eventType) { + this.folderPath = folderPath; + this.eventType = eventType; + } + + @Override + public String toString() { + return "{" + + "\"folderPath\" : \"" + folderPath + "\" ," + + "\"eventType\" : \"" + eventType + "\"" + + "}"; + } + } +} diff --git a/Java/springboot/src/main/resources/application.properties b/Java/springboot/src/main/resources/application.properties index 37ebf86..0ad8afb 100644 --- a/Java/springboot/src/main/resources/application.properties +++ b/Java/springboot/src/main/resources/application.properties @@ -13,4 +13,7 @@ webdav.showExceptions=true webdav.rootFolder= # Your WebDAV server is available at the context specified in this variable. There must be trailing slash ("/"). -webdav.rootContext=/DAV/ \ No newline at end of file +webdav.rootContext=/DAV/ + +# WebSockets are available at this endpoint. WebSockets are used in the default GET page. +webdav.rootWebSocket=/ \ No newline at end of file diff --git a/Java/springboot/src/main/resources/handler/MyCustomHandlerPage.html b/Java/springboot/src/main/resources/handler/MyCustomHandlerPage.html index 6c50be4..60c708d 100644 --- a/Java/springboot/src/main/resources/handler/MyCustomHandlerPage.html +++ b/Java/springboot/src/main/resources/handler/MyCustomHandlerPage.html @@ -294,5 +294,6 @@ + diff --git a/Kotlin/filesystemstorage/pom.xml b/Kotlin/filesystemstorage/pom.xml index 54993f0..4cce779 100644 --- a/Kotlin/filesystemstorage/pom.xml +++ b/Kotlin/filesystemstorage/pom.xml @@ -6,7 +6,7 @@ com.ithit.webdav.samples kotlinfs - 4.3.3576 + 4.3.3604 war @@ -34,7 +34,7 @@ com.ithit.webdav.integration servlet-integration - 4.3.3576 + 4.3.3604 commons-io @@ -78,12 +78,12 @@ org.apache.tika tika-core - 1.22 + 1.23 org.apache.tika tika-parsers - 1.22 + 1.23 cxf-core @@ -142,7 +142,7 @@ com.ithit.webdav webdav-server - 4.3.3576 + 4.3.3604 net.java.dev.jna @@ -240,7 +240,7 @@ filesystem 11021 / - target/kotlinfs-4.3.3576 + target/kotlinfs-4.3.3604