Commit 12fcf1b2 authored by Julien's avatar Julien

Merge pull request #1724 from bonitasoft/feat/import-artifact

Feat/import artifact
parents f81599a6 0fba0fd5
......@@ -18,7 +18,9 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Validation;
import javax.validation.Validator;
......@@ -26,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.bonitasoft.web.designer.controller.asset.AssetService;
import org.bonitasoft.web.designer.controller.export.Exporter;
......@@ -176,13 +179,21 @@ public class DesignerConfig {
}
@Bean
public ArtifactImporter<Page> pageImporter(Unzipper unzip, PageRepository pageRepository, WidgetImporter widgetImporter, AssetImporter<Page> pageAssetImporter) {
return new ArtifactImporter<>(unzip, pageRepository, pageFileBasedLoader(), widgetImporter, pageAssetImporter);
public ArtifactImporter<Page> pageImporter(PageRepository pageRepository, WidgetImporter widgetImporter, AssetImporter<Page> pageAssetImporter) {
return new ArtifactImporter<>(pageRepository, pageFileBasedLoader(), widgetImporter, pageAssetImporter);
}
@Bean
public ArtifactImporter<Widget> widgetImporter(Unzipper unzip, WidgetLoader widgetLoader, WidgetRepository widgetRepository, AssetImporter<Widget> widgetAssetImporter) {
return new ArtifactImporter<>(unzip, widgetRepository, widgetLoader, widgetAssetImporter);
public ArtifactImporter<Widget> widgetImporter(WidgetLoader widgetLoader, WidgetRepository widgetRepository, AssetImporter<Widget> widgetAssetImporter) {
return new ArtifactImporter<>(widgetRepository, widgetLoader, widgetAssetImporter);
}
@Bean
public Map<String, ArtifactImporter> artifactImporters(ArtifactImporter<Page> pageImporter, ArtifactImporter<Widget> widgetImporter) {
return ImmutableMap.<String, ArtifactImporter>builder()
.put("page", pageImporter)
.put("widget", widgetImporter)
.build();
}
@Bean
......
......@@ -14,14 +14,17 @@
*/
package org.bonitasoft.web.designer.controller;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import org.bonitasoft.web.designer.controller.importer.ArtifactImporter;
import org.bonitasoft.web.designer.controller.importer.Import;
import org.bonitasoft.web.designer.controller.importer.ImportStore;
import org.bonitasoft.web.designer.controller.importer.MultipartFileImporter;
import org.bonitasoft.web.designer.controller.importer.report.ImportReport;
import org.bonitasoft.web.designer.model.page.Page;
import org.bonitasoft.web.designer.model.widget.Widget;
import org.bonitasoft.web.designer.controller.utils.MimeType;
import org.bonitasoft.web.designer.repository.exception.NotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
......@@ -37,17 +40,17 @@ import org.springframework.web.multipart.MultipartFile;
public class ImportController {
private MultipartFileImporter multipartFileImporter;
private ArtifactImporter<Page> pageImporter;
private ArtifactImporter<Widget> widgetImporter;
private Map<String, ArtifactImporter> artifactImporters;
private ImportStore importStore;
@Inject
public ImportController(
MultipartFileImporter multipartFileImporter,
@Named("pageImporter") ArtifactImporter<Page> pageImporter,
@Named("widgetImporter") ArtifactImporter<Widget> widgetImporter) {
@Value("#{artifactImporters}") Map<String, ArtifactImporter> artifactImporters,
ImportStore importStore) {
this.multipartFileImporter = multipartFileImporter;
this.pageImporter = pageImporter;
this.widgetImporter = widgetImporter;
this.artifactImporters = artifactImporters;
this.importStore = importStore;
}
/*
......@@ -55,42 +58,55 @@ public class ImportController {
* We need to force it to text/plain for browser not trying to save it and pass it correctly to application.
* Using text/plain as content-type header in response doesn't affect other browsers.
*/
@RequestMapping(value = "/import/page", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@RequestMapping(value = "/import/{artifactType}", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ImportReport importPage(@RequestParam("file") MultipartFile file, @RequestParam(value = "force", defaultValue = "false", required = false) boolean force) {
return multipartFileImporter.importFile(file, pageImporter, force);
public ImportReport importArtifact(@RequestParam("file") MultipartFile file,
@RequestParam(value = "force", defaultValue = "false", required = false) boolean force,
@PathVariable(value = "artifactType") String artifactType) {
checkFilePartIsPresent(file);
checkFileIsZip(file);
ArtifactImporter importer = getArtifactImporter(artifactType);
return multipartFileImporter.importFile(file, importer, force);
}
@RequestMapping(value = "/import/page/{uuid}", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@RequestMapping(value = "/import/{uuid}/force", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ImportReport importPage(@PathVariable("uuid") String uuid) {
return pageImporter.forceExecution(uuid);
Import anImport = importStore.get(uuid);
return anImport.getImporter().forceImport(anImport);
}
@RequestMapping(value = "/import/page/cancel/{uuid}", method = RequestMethod.GET)
@RequestMapping(value = "/import/{uuid}/cancel", method = RequestMethod.POST)
public void cancelPageImport(@PathVariable("uuid") String uuid) {
pageImporter.cancelImport(uuid);
importStore.remove(uuid);
}
@RequestMapping(value = "/import/widget", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ImportReport importWidget(@RequestParam("file") MultipartFile file, @RequestParam(value = "force", defaultValue = "false", required = false) boolean force) {
return multipartFileImporter.importFile(file, widgetImporter, force);
private ArtifactImporter getArtifactImporter(String artifactType) {
ArtifactImporter importer = artifactImporters.get(artifactType);
if (importer == null) {
throw new NotFoundException();
}
return importer;
}
@RequestMapping(value = "/import/widget/{uuid}", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ImportReport importWidget(@PathVariable("uuid") String uuid) {
return widgetImporter.forceExecution(uuid);
private void checkFilePartIsPresent(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Part named [file] is needed to successfully import a component");
}
}
@RequestMapping(value = "/import/widget/cancel/{uuid}", method = RequestMethod.GET)
public void cancelWidgetImport(@PathVariable("uuid") String uuid) {
widgetImporter.cancelImport(uuid);
private void checkFileIsZip(MultipartFile file) {
if (isNotZipFile(file)) {
throw new IllegalArgumentException("Only zip files are allowed when importing a component");
}
}
// some browsers send application/octet-stream for zip files
// so we check that mimeType is application/zip or application/octet-stream and filename ends with .zip
private boolean isNotZipFile(MultipartFile file) {
return !MimeType.APPLICATION_ZIP.matches(file.getContentType())
&& !(MimeType.APPLICATION_OCTETSTREAM.matches(file.getContentType()) && file.getOriginalFilename().endsWith(".zip"));
}
}
......@@ -16,25 +16,19 @@ package org.bonitasoft.web.designer.controller.importer;
import static java.lang.String.format;
import static java.nio.file.Files.notExists;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.bonitasoft.web.designer.controller.importer.ImportException.Type.*;
import static org.bonitasoft.web.designer.controller.importer.ImportException.Type.JSON_STRUCTURE;
import static org.bonitasoft.web.designer.controller.importer.ImportException.Type.PAGE_NOT_FOUND;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipException;
import org.apache.commons.lang3.StringUtils;
import org.bonitasoft.web.designer.controller.importer.ImportException.Type;
import org.bonitasoft.web.designer.controller.importer.dependencies.DependencyImporter;
import org.bonitasoft.web.designer.controller.importer.report.ImportReport;
import org.bonitasoft.web.designer.controller.utils.Unzipper;
import org.bonitasoft.web.designer.model.Identifiable;
import org.bonitasoft.web.designer.repository.Loader;
import org.bonitasoft.web.designer.repository.Repository;
......@@ -48,68 +42,29 @@ public class ArtifactImporter<T extends Identifiable> {
protected static final Logger logger = LoggerFactory.getLogger(ArtifactImporter.class);
private Unzipper unzip;
private Repository<T> repository;
private Loader<T> loader;
private DependencyImporter[] dependencyImporters;
private Map<String, String> extractedDirPathMap = new ConcurrentHashMap<>();
public ArtifactImporter(Unzipper unzip, Repository<T> repository, Loader<T> loader, DependencyImporter... dependencyImporters) {
public ArtifactImporter(Repository<T> repository, Loader<T> loader, DependencyImporter... dependencyImporters) {
this.loader = loader;
this.repository = repository;
this.unzip = unzip;
this.dependencyImporters = dependencyImporters;
}
public void cancelImport(String uuid) {
String dir = extractedDirPathMap.remove(uuid);
if (StringUtils.isNotBlank(dir)) {
deleteQuietly(unzip.getTemporaryZipPath().resolve(dir).toFile());
}
}
public ImportReport execute(InputStream is, boolean force) {
Path extractDir = unzip(is);
return (force) ? forceImportFromPath(extractDir) : importFromPath(extractDir);
}
public ImportReport forceExecution(String uuid) {
if (extractedDirPathMap.get(uuid) != null) {
Path extractDir = unzip.getTemporaryZipPath().resolve(extractedDirPathMap.get(uuid));
return forceImportFromPath(extractDir);
}
throw new UnsupportedOperationException("Cannot forceExecution import with null UUID");
public ImportReport forceImport(Import anImport) {
return tryToImportAndGenerateReport(anImport, true);
}
public ImportReport forceImportFromPath(Path extractDir) {
Path resources = getPath(extractDir);
try {
return tryToImportAndGenerateReport(resources, null);
} finally {
deleteQuietly(extractDir.toFile());
}
}
public ImportReport importFromPath(Path extractDir) {
Path resources = getPath(extractDir);
ImportReport importReport = null;
try {
importReport = tryToImportAndGenerateReport(resources, extractDir);
return importReport;
} finally {
if (importReport == null || StringUtils.isBlank(importReport.getUUID())) {
deleteQuietly(extractDir.toFile());
} else {
extractDir.toFile().deleteOnExit();
}
}
public ImportReport doImport(Import anImport) {
return tryToImportAndGenerateReport(anImport, false);
}
/*
* if uploadedFileDirectory is null, the import is forced
*/
private ImportReport tryToImportAndGenerateReport(Path resources, Path uploadedFileDirectory) {
private ImportReport tryToImportAndGenerateReport(Import anImport, boolean force) {
Path resources = getPath(anImport.getPath());
String modelFile = repository.getComponentName() + ".json";
try {
// first load everything
......@@ -117,16 +72,15 @@ public class ArtifactImporter<T extends Identifiable> {
Map<DependencyImporter, List<?>> dependencies = loadArtefactDependencies(element, resources);
ImportReport report = buildReport(element, dependencies);
report.setUUID(anImport.getUuid());
if (uploadedFileDirectory == null
|| (report.doesNotOverrideElements())) {
if (force || report.doesNotOverrideElements()) {
// then save them
saveArtefactDependencies(resources, dependencies);
repository.updateLastUpdateAndSave(element);
report.setStatus(ImportReport.Status.IMPORTED);
} else {
String uuid = UUID.randomUUID().toString();
extractedDirPathMap.put(uuid, uploadedFileDirectory.getFileName().toFile().getName());
report.setUUID(uuid);
report.setStatus(ImportReport.Status.CONFLICT);
}
return report;
} catch (IOException | RepositoryException e) {
......@@ -176,15 +130,4 @@ public class ArtifactImporter<T extends Identifiable> {
return map;
}
private Path unzip(InputStream is) {
try {
return unzip.unzipInTempDir(is, "pageDesignerImport");
} catch (ZipException e) {
logger.error("Cannot open zip file", e);
throw new ImportException(CANNOT_OPEN_ZIP, "Cannot open zip file", e);
} catch (IOException e) {
throw new ServerImportException("Error while unzipping zip file", e);
}
}
}
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.controller.importer;
import java.nio.file.Path;
public class Import {
private ArtifactImporter importer;
private String uuid;
private Path path;
public Import(ArtifactImporter importer, String uuid, Path path) {
this.importer = importer;
this.uuid = uuid;
this.path = path;
}
public ArtifactImporter getImporter() {
return importer;
}
public String getUuid() {
return uuid;
}
public Path getPath() {
return path;
}
}
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.controller.importer;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Named;
import org.bonitasoft.web.designer.repository.exception.NotFoundException;
@Named
public class ImportStore {
private Map<String, Import> extractedDirPathMap = new ConcurrentHashMap<>();
public Import store(ArtifactImporter importer, Path path) {
path.toFile().deleteOnExit(); // whatever happen, this directory should be deleted when app server exit
String uuid = UUID.randomUUID().toString();
Import anImport = new Import(importer, uuid, path);
extractedDirPathMap.put(uuid, anImport);
return anImport;
}
public void remove(String uuid) {
Import anImport = extractedDirPathMap.remove(uuid);
if (anImport != null) {
deleteQuietly(anImport.getPath().toFile());
}
}
public Import get(String uuid) {
Import anImport = extractedDirPathMap.get(uuid);
if (anImport == null) {
throw new NotFoundException();
}
return anImport;
}
}
......@@ -14,44 +14,80 @@
*/
package org.bonitasoft.web.designer.controller.importer;
import static org.bonitasoft.web.designer.controller.importer.ImportException.Type.CANNOT_OPEN_ZIP;
import static org.bonitasoft.web.designer.controller.importer.report.ImportReport.Status.IMPORTED;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.zip.ZipException;
import javax.inject.Inject;
import javax.inject.Named;
import org.bonitasoft.web.designer.controller.importer.ImportException.Type;
import com.google.common.base.Function;
import org.bonitasoft.web.designer.controller.importer.report.ImportReport;
import org.bonitasoft.web.designer.controller.utils.MimeType;
import org.bonitasoft.web.designer.controller.utils.Unzipper;
import org.springframework.web.multipart.MultipartFile;
@Named
public class MultipartFileImporter {
public ImportReport importFile(MultipartFile file, ArtifactImporter importer, boolean force) {
try {
return doImport(file, importer, force);
} catch (IOException | IllegalArgumentException e) {
throw new ImportException(Type.SERVER_ERROR, e.getMessage(), e);
}
private Unzipper unzip;
private ImportStore importStore;
@Inject
public MultipartFileImporter(Unzipper unzip, ImportStore importStore) {
this.unzip = unzip;
this.importStore = importStore;
}
public ImportReport importFile(MultipartFile file, final ArtifactImporter importer, boolean force) {
Path extractDir = unzip(file);
Function importFn = getImportFunction(importer, force);
return importFromPath(extractDir, importer, importFn);
}
private ImportReport doImport(MultipartFile file, ArtifactImporter importer, boolean force) throws IOException {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Part named [file] is needed to successfully import a component");
private Function getImportFunction(final ArtifactImporter importer, boolean force) {
if (force) {
return new Function<Import, ImportReport>() {
@Override
public ImportReport apply(Import anImport) {
return importer.forceImport(anImport);
}
};
} else {
return new Function<Import, ImportReport>() {
@Override
public ImportReport apply(Import anImport) {
return importer.doImport(anImport);
}
};
}
}
if (isNotZipFile(file)) {
throw new IllegalArgumentException("Only zip files are allowed when importing a component");
private ImportReport importFromPath(Path extractDir, ArtifactImporter importer, Function<Import, ImportReport> importFn) {
Import anImport = importStore.store(importer, extractDir);
ImportReport report = null;
try {
report = importFn.apply(anImport);
} finally {
if (report == null || IMPORTED.equals(report.getStatus())) {
importStore.remove(anImport.getUuid());
}
}
return report;
}
private Path unzip(MultipartFile file) {
try (InputStream is = file.getInputStream()) {
return importer.execute(is, force);
return unzip.unzipInTempDir(is, "pageDesignerImport");
} catch (ZipException e) {
throw new ImportException(CANNOT_OPEN_ZIP, "Cannot open zip file", e);
} catch (IOException e) {
throw new ServerImportException("Error while unzipping zip file", e);
}
}
// some browsers send application/octet-stream for zip files
// so we check that mimeType is application/zip or application/octet-stream and filename ends with .zip
private boolean isNotZipFile(MultipartFile file) {
return !MimeType.APPLICATION_ZIP.matches(file.getContentType())
&& !(MimeType.APPLICATION_OCTETSTREAM.matches(file.getContentType()) && file.getOriginalFilename().endsWith(".zip"));
}
}
......@@ -17,6 +17,7 @@ package org.bonitasoft.web.designer.controller.importer.report;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonValue;
import org.bonitasoft.web.designer.controller.importer.dependencies.DependencyImporter;
import org.bonitasoft.web.designer.model.Identifiable;
......@@ -24,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public class ImportReport {
private Status status;
private Identifiable element;
private Boolean overridden = false;
private Dependencies dependencies;
......@@ -74,4 +76,21 @@ public class ImportReport {
public boolean doesNotOverrideElements() {
return !this.isOverridden() && (this.getDependencies().getOverridden() == null || this.getDependencies().getOverridden().isEmpty());
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public enum Status {
IMPORTED, CONFLICT;
@JsonValue
public String toString() {
return name().toLowerCase();
}
}
}
......@@ -20,4 +20,6 @@ public class NotFoundException extends RuntimeException {
super(message);
}
public NotFoundException() {
}
}
......@@ -27,6 +27,7 @@ public class ImportReportBuilder {
List<Identifiable> added = new ArrayList<>();
List<Identifiable> overridden = new ArrayList<>();
private String uuid;
private ImportReport.Status status;
private boolean override;
......@@ -62,6 +63,11 @@ public class ImportReportBuilder {
return this;
}
public ImportReportBuilder withStatus(ImportReport.Status status) {
this.status = status;
return this;
}
public ImportReport build() {
Dependencies dependencies = new Dependencies();
for (Identifiable identifiable : added) {
......@@ -73,6 +79,7 @@ public class ImportReportBuilder {
ImportReport importReport = new ImportReport(element, dependencies);
importReport.setUUID(uuid);
importReport.setOverridden(override);
importReport.setStatus(status);
return importReport;
}
}
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.controller.importer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.bonitasoft.web.designer.utils.assertions.CustomAssertions.assertThat;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.bonitasoft.web.designer.repository.exception.NotFoundException;
import org.bonitasoft.web.designer.utils.rule.TemporaryFolder;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ImportStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock
private ArtifactImporter artifactImporter;
private ImportStore importStore;
@Before
public void setUp() throws Exception {
importStore = new ImportStore();
}
@Test
public void should_store_import() throws Exception {
Path importPath = Paths.get("import/path");
Import storedImport = importStore.store(artifactImporter, importPath);
assertThat(storedImport.getUuid()).isNotNull();
assertThat(storedImport.getImporter()).isEqualTo(artifactImporter);
assertThat(storedImport.getPath()).isEqualTo(importPath);
}
@Test
public void should_get_a_stored_import() throws Exception {
Import expectedImport = importStore.store(artifactImporter, Paths.get("import/path"));