[RM] Feature Ability to delete the Service logs via GUI
MODIFY
BackEnd/Core/sailfish-core/src/main/java/com/exactpro/sf/configuration/workspace/DefaultWorkspaceDispatcher.java
BackEnd/Core/sailfish-core/src/main/java/com/exactpro/sf/configuration/workspace/DefaultWorkspaceDispatcher.java
@@ -23,7 +23,6 @@ import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
@@ -44,7 +43,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.exactpro.sf.common.util.Pair;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
public class DefaultWorkspaceDispatcher implements IWorkspaceDispatcher {
@@ -214,6 +212,7 @@ public class DefaultWorkspaceDispatcher implements IWorkspaceDispatcher {
/**
* Create file in the last workspace
+ *
* @throws WorkspaceStructureException
*/
@Override
@@ -394,6 +393,7 @@ public class DefaultWorkspaceDispatcher implements IWorkspaceDispatcher {
/**
* Find file in workspaces (search order: form last workspace to deploy folder)
+ *
* @param folderType
* @param fileName
* @return
@@ -406,15 +406,7 @@ public class DefaultWorkspaceDispatcher implements IWorkspaceDispatcher {
for (int i = workspaces.size() - 1; i >= 0; i--) {
WorkspaceLayer layer = workspaces.get(i);
targetFile = new File(layer.get(folderType), toPathString(fileName));
-
- Path targetPath = targetFile.toPath().normalize();
- Path rootPath = workspaces.get(i).get(folderType).toPath().normalize();
- if (! targetPath.startsWith(rootPath)) {
- throw new WorkspaceSecurityException("Access outside workspace is denied. Access file: {" + folderType + "}/" + toPathString(fileName));
- }
-
-
- if (targetFile.exists()) {
+ if (existsOnLayer(folderType, layer, fileName)) {
return Maps.immutableEntry(layer, targetFile);
}
}
@@ -536,6 +528,34 @@ public class DefaultWorkspaceDispatcher implements IWorkspaceDispatcher {
return targetFile;
}
+ @Override
+ public boolean isLastLayerFile(FolderType folderType, String... fileName) throws IOException {
+ checkFolderTypeAndFileName(folderType, fileName);
+
+ if (workspaces.size() <= 1) {
+ return false;
+ }
+
+ try {
+ workspacesLock.readLock().lock();
+ return existsOnLayer(folderType, workspaces.get(workspaces.size() - 1), fileName);
+ } finally {
+ workspacesLock.readLock().unlock();
+ }
+ }
+
+ private boolean existsOnLayer(FolderType folderType, WorkspaceLayer layer, String... fileName) {
+ File targetFile = new File(layer.get(folderType), toPathString(fileName));
+
+ Path targetPath = targetFile.toPath().normalize();
+ Path rootPath = layer.get(folderType).toPath().normalize();
+ if (!targetPath.startsWith(rootPath)) {
+ throw new WorkspaceSecurityException("Access outside workspace is denied. Access file: {" + folderType + "}/" + toPathString(fileName));
+ }
+
+ return targetFile.exists();
+ }
+
private void checkFolderTypeAndFileName(FolderType folderType, String... fileName) {
checkFolderType(folderType);
checkFileName(fileName);

MODIFY
BackEnd/Core/sailfish-core/src/main/java/com/exactpro/sf/configuration/workspace/IWorkspaceDispatcher.java
BackEnd/Core/sailfish-core/src/main/java/com/exactpro/sf/configuration/workspace/IWorkspaceDispatcher.java
@@ -167,4 +167,12 @@ public interface IWorkspaceDispatcher {
* @return writeable file from last layer
*/
File getWritableFile(FolderType folderType, String... fileName) throws WorkspaceSecurityException, IOException;
+
+ /**
+ * Checks whether the file is on the last layer. If the last layer is the only one, returns false.
+ * @return
+ */
+ default boolean isLastLayerFile(FolderType folderType, String... fileName) throws IOException {
+ throw new UnsupportedOperationException("Method 'isLastLayerFile' isn't implement.");
+ }
}

MODIFY
BackEnd/Core/sailfish-core/src/main/kotlin/com/exactpro/sf/externalapi/codec/impl/AbstractExternalMinaCodecFactory.kt
BackEnd/Core/sailfish-core/src/main/kotlin/com/exactpro/sf/externalapi/codec/impl/AbstractExternalMinaCodecFactory.kt
@@ -206,6 +206,7 @@ abstract class AbstractExternalMinaCodecFactory : AbstractExternalCodecFactory()
override fun listFilesAsFiles(filter: FileFilter, folderType: FolderType, recursive: Boolean, vararg fileName: String): Set<File> = throw UnsupportedOperationException()
override fun removeFolder(folderType: FolderType, vararg fileName: String): Unit = throw UnsupportedOperationException()
override fun createFile(folderType: FolderType, overwrite: Boolean, vararg fileName: String): File = throw UnsupportedOperationException()
+ override fun isLastLayerFile(folderType: FolderType, vararg fileName: String): Boolean = throw UnsupportedOperationException()
override fun getOrCreateFile(folderType: FolderType, vararg fileName: String): File = throw UnsupportedOperationException()
override fun getFolder(folderType: FolderType): File = throw UnsupportedOperationException()
override fun getWritableFile(folderType: FolderType, vararg fileName: String): File = throw UnsupportedOperationException()

MODIFY
FrontEnd/SailfishFrontEnd/WebContent/download.xhtml
FrontEnd/SailfishFrontEnd/WebContent/download.xhtml
+<!--/******************************************************************************-->
+<!-- * Copyright 2009-2018 Exactpro (Exactpro Systems Limited)-->
+<!-- *-->
+<!-- * Licensed under the Apache License, Version 2.0 (the "License");-->
+<!-- * you may not use this file except in compliance with the License.-->
+<!-- * You may obtain a copy of the License at-->
+<!-- *-->
+<!-- * http://www.apache.org/licenses/LICENSE-2.0-->
+<!-- *-->
+<!-- * Unless required by applicable law or agreed to in writing, software-->
+<!-- * distributed under the License is distributed on an "AS IS" BASIS,-->
+<!-- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.-->
+<!-- * See the License for the specific language governing permissions and-->
+<!-- * limitations under the License.-->
+<!-- ******************************************************************************/-->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui"
xmlns:ui="http://java.sun.com/jsf/facelets"
- xmlns:c="http://java.sun.com/jstl/core">
+>
<h:head>
<link rel="shortcut icon" href="#{resource['sf:images/SFicon.ico']}" type="image/x-icon"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
console.error = console.error || function(){};
console.info = console.info || function(){};
+
</script>
</h:head>
<p:fileDownload value="#{DownloadBean.zipContent}"/>
</p:menuitem>
<p:menuitem id="showTextButton" icon="eps-custom-icon eps-custom-icon-view" ajax="false" disabled="#{DownloadBean.showTextNotAvailable}" actionListener="#{DownloadBean.showTextContentModal}"/>
+ <p:menuitem id="deleteButton" icon="eps-custom-icon eps-custom-icon-delete" ajax="false" disabled="#{DownloadBean.deleteFilesNotAvailable}" onclick="PF('confirmation').show(); return false;"/>
</p:menubar>
<p:tooltip value="Download selected folders and files" for="downloadButton"/>
<p:tooltip value="Download selected as ZIP" for="downloadZipButton"/>
<p:tooltip value="Show file content" for="showTextButton"/>
+ <p:tooltip value="Delete file(s)" for="deleteButton"/>
</p:outputPanel>
rowKey="#{file.name}" scrollable="true" scrollHeight="100%" resizableColumns="true"
rowStyleClass="eps-download-row #{file.directory ? 'directory-row' : 'file-row'}" styleClass="eps-main-table eps-download-table">
- <p:ajax event="rowSelect" update=":form:download-header" />
- <p:ajax event="rowUnselect" update=":form:download-header" />
- <p:ajax event="rowSelectCheckbox" update=":form:download-header" />
- <p:ajax event="rowUnselectCheckbox" update=":form:download-header" />
- <p:ajax event="toggleSelect" update=":form:download-header" />
- <p:ajax event="rowDblselect" listener="#{DownloadBean.toSelected}" update=":form:mainMenuBar :form:curDirP folderContent :form:download-header"
+ <p:ajax event="rowSelect" update=":form:download-header :confirmationForm:deleteFilesList"/>
+ <p:ajax event="rowUnselect" update=":form:download-header :confirmationForm:deleteFilesList"/>
+ <p:ajax event="rowSelectCheckbox" update=":form:download-header :confirmationForm:deleteFilesList"/>
+ <p:ajax event="rowUnselectCheckbox" update=":form:download-header :confirmationForm:deleteFilesList"/>
+ <p:ajax event="toggleSelect" update=":form:download-header :confirmationForm:deleteFilesList"/>
+ <p:ajax event="rowDblselect" listener="#{DownloadBean.toSelected}" update=":form:mainMenuBar :form:curDirP folderContent :form:download-header :confirmationForm:deleteFilesList"
oncomplete="onSortComplete()"/>
<p:ajax event="sort" oncomplete="onSortComplete()"/>
<h:graphicImage library="sf" name="images/download/isDir/#{file.directory}.png"/>
</p:column>
- <p:column headerText="Name" sortBy="#{file.name}" style="width: 550px">
+ <p:column headerText="Name" sortBy="#{file.name}" style="width: 540px">
<h:outputText value="#{file.name}"/>
</p:column>
<h:outputText value="#{file.size}"/>
</p:column>
+ <p:column style="width: 10px">
+ <h:outputText value="#{DownloadBean.isDeleteFileAvailable(file) ? '+' : '' }"/>
+ </p:column>
+
</p:dataTable>
<br/>
+
</h:form>
</div>
</p:dialog>
</h:form>
+ <h:form enctype="multipart/form-data" id="confirmationForm">
+ <p:dialog header="Delete file(s)" widgetVar="confirmation" modal="true" resizable="false">
+ <div style="text-align: center; margin: 10px auto;">
+ <h:outputText value="Are you sure about deleting selected files?"/>
+ </div>
+ <h:panelGroup id="deleteFilesList" styleClass="eps-delete-files-list-wrap">
+ <ul class="eps-delete-files-list">
+ <ui:repeat value="#{DownloadBean.getTopSelectedFiles(10)}" var="file">
+ <li>
+ <h:outputText value="#{file.name}"/>
+ </li>
+ </ui:repeat>
+ </ul>
+ <h:outputText value="... and #{DownloadBean.getNumberOfSelectedFiles() - 10} more files." rendered="#{DownloadBean.getNumberOfSelectedFiles() > 10}"/>
+ </h:panelGroup>
+
+
+ <div style="text-align: left; margin: 5px auto;">
+ <h:outputText value="*Attention! Files can be deleted only from the last layer."/>
+ </div>
+
+ <div style="text-align: center;">
+ <p:commandButton id="deleteSubmit" value="Yes" action="#{DownloadBean.deleteFiles}" oncomplete="PF('confirmation').hide()" update=":form"/>
+ <p:commandButton id="deleteCancel" value="No" oncomplete="PF('confirmation').hide()"/>
+ </div>
+ </p:dialog>
+ </h:form>
+
</h:body>
</html>
\ No newline at end of file

MODIFY
FrontEnd/SailfishFrontEnd/WebContent/resources/sf/css/download.css
FrontEnd/SailfishFrontEnd/WebContent/resources/sf/css/download.css
+/******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
.highlight {
background-color: #FFFF88;
border-left: 1px solid #cdcdcd;
border-right: 1px solid #cdcdcd;
}
+
+.eps-delete-files-list-wrap {
+ margin: 10px auto;
+}
+
+ul.eps-delete-files-list {
+ height: auto;
+ width: 100%;
+ box-sizing: border-box;
+ margin: 10px auto 0 auto;
+ padding: 4px 30px;
+ list-style: square;
+}
+
+ul.eps-delete-files-list li {
+ height: auto;
+ width: 100%;
+ padding: 2px;
+ margin: 1px 0;
+ border-bottom: 1px solid #eee;
+}
+
+ul.eps-delete-files-list li:last-child {
+ border: none;
+}
\ No newline at end of file

MODIFY
FrontEnd/SailfishFrontEnd/src/main/java/com/exactpro/sf/testwebgui/download/DownloadBean.java
FrontEnd/SailfishFrontEnd/src/main/java/com/exactpro/sf/testwebgui/download/DownloadBean.java
@@ -21,8 +21,12 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
@@ -303,6 +307,66 @@ public class DownloadBean implements Serializable {
return selectedFiles.length != 1 || selectedFiles[0].isDirectory();
}
+ public boolean isDeleteFileAvailable(FileAdapter fileAdapter) {
+ if (fileAdapter.isDirectory()) {
+ return false;
+ }
+
+ IWorkspaceDispatcher workspaceDispatcher = BeanUtil.findBean(BeanUtil.WORKSPACE_DISPATCHER, IWorkspaceDispatcher.class);
+
+ try {
+ return workspaceDispatcher.isLastLayerFile(FolderType.ROOT, curDirPath, fileAdapter.getFile().getName());
+ } catch (Exception e) {
+ logger.warn("Unable to get information about file {}", fileAdapter.getFile().getName(), e);
+ }
+ return false;
+ }
+
+ public boolean isDeleteFilesNotAvailable() {
+ if (Stream.of(selectedFiles).anyMatch(FileAdapter::isDirectory)) {
+ return true;
+ }
+
+ IWorkspaceDispatcher workspaceDispatcher = BeanUtil.findBean(BeanUtil.WORKSPACE_DISPATCHER, IWorkspaceDispatcher.class);
+
+ return selectedFiles.length == 0 || Stream.of(selectedFiles)
+ .map(FileAdapter::getFile).anyMatch(file ->
+ {
+ try {
+ return !workspaceDispatcher.isLastLayerFile(FolderType.ROOT, curDirPath, file.getName());
+ } catch (Exception e) {
+ logger.warn("Unable to get information about file {}", file.getName(), e);
+ return true;
+ }
+ });
+ }
+
+ public void deleteFiles() throws FileNotFoundException {
+ if (selectedFiles.length > 0) {
+ IWorkspaceDispatcher workspaceDispatcher = BeanUtil.findBean(BeanUtil.WORKSPACE_DISPATCHER, IWorkspaceDispatcher.class);
+
+ for (FileAdapter fa : selectedFiles) {
+ try {
+ workspaceDispatcher.removeFile(FolderType.ROOT, curDirPath, fa.getName());
+ } catch (Exception e) {
+ logger.warn("Unable to remove file {}", fa.getName(), e);
+ }
+ }
+ fillFiles();
+ }
+ }
+
+ public List<FileAdapter> getTopSelectedFiles(int number) {
+ return Arrays.stream(selectedFiles)
+ .limit(number)
+ .sorted(Comparator.comparing(FileAdapter::getName, String.CASE_INSENSITIVE_ORDER))
+ .collect(Collectors.toList());
+ }
+
+ public int getNumberOfSelectedFiles() {
+ return selectedFiles.length;
+ }
+
public void showTextContentModal() {
loadPage("new");

[RM] Feature #64908. Ability to delete the Service logs via GUI

Change-Id: Iffd0fd20bcc464bfe334ce87fc843f28f978a40f