Commit b028d73b authored by Colin PUY's avatar Colin PUY

Check that json is valid while uploading a json asset

parent 2ce586c6
......@@ -257,12 +257,12 @@ public class DesignerConfig {
@Bean
public AssetService<Page> pageAssetService(PageRepository pageRepository) {
return new AssetService<>(pageRepository, pageAssetRepository(pageRepository), pageAssetImporter(pageAssetRepository(pageRepository)));
return new AssetService<>(pageRepository, pageAssetRepository(pageRepository), pageAssetImporter(pageAssetRepository(pageRepository)), objectMapperWrapper());
}
@Bean
public AssetService<Widget> widgetAssetService(WidgetRepository widgetRepository) {
return new AssetService<>(widgetRepository, widgetAssetRepository(widgetRepository), widgetAssetImporter(widgetAssetRepository(widgetRepository)));
return new AssetService<>(widgetRepository, widgetAssetRepository(widgetRepository), widgetAssetImporter(widgetAssetRepository(widgetRepository)), objectMapperWrapper());
}
@Bean
......
......@@ -21,6 +21,7 @@ import static org.bonitasoft.web.designer.controller.asset.AssetService.OrderTyp
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Preconditions;
import org.bonitasoft.web.designer.controller.asset.AssetService;
import org.bonitasoft.web.designer.model.Assetable;
......
......@@ -55,6 +55,13 @@ public class ErrorMessage {
}
}
public void addInfo(String key, Object value) {
if (this.infos == null) {
this.infos = new HashMap<>();
}
this.infos.put(key, value);
}
public Map<String, Object> getInfos() {
return infos;
}
......
......@@ -14,6 +14,7 @@
*/
package org.bonitasoft.web.designer.controller;
import org.bonitasoft.web.designer.controller.asset.MalformedJsonException;
import org.bonitasoft.web.designer.controller.importer.ImportException;
import org.bonitasoft.web.designer.repository.exception.ConstraintValidationException;
import org.bonitasoft.web.designer.repository.exception.InUseException;
......@@ -99,4 +100,13 @@ public class ResourceControllerAdvice {
errorMessage.addInfos(exception.getInfos());
return new ResponseEntity<>(errorMessage, HttpStatus.ACCEPTED);
}
@ExceptionHandler(MalformedJsonException.class)
public ResponseEntity<ErrorMessage> handleJsonProcessingException(MalformedJsonException exception) {
logger.error("Error while uploading a json file " + exception.getMessage());
// BS-14113: HttpStatus.ACCEPTED internet explorer don't recognize response if sent with http error code
ErrorMessage message = new ErrorMessage(exception);
message.addInfo("location", exception.getLocation());
return new ResponseEntity<>(message, HttpStatus.ACCEPTED);
}
}
......@@ -26,14 +26,15 @@ import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import org.bonitasoft.web.designer.controller.importer.ServerImportException;
import org.bonitasoft.web.designer.controller.importer.dependencies.AssetImporter;
import org.bonitasoft.web.designer.model.Assetable;
import org.bonitasoft.web.designer.model.JacksonObjectMapper;
import org.bonitasoft.web.designer.model.asset.Asset;
import org.bonitasoft.web.designer.model.asset.AssetType;
import org.bonitasoft.web.designer.model.page.Previewable;
......@@ -45,7 +46,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
public class AssetService<T extends Assetable> {
protected static final Logger logger = LoggerFactory.getLogger(AssetService.class);
......@@ -58,11 +58,13 @@ public class AssetService<T extends Assetable> {
private Repository<T> repository;
private AssetRepository<T> assetRepository;
private AssetImporter<T> assetImporter;
private JacksonObjectMapper mapper;
public AssetService(Repository<T> repository, AssetRepository<T> assetRepository, AssetImporter<T> assetImporter) {
public AssetService(Repository<T> repository, AssetRepository<T> assetRepository, AssetImporter<T> assetImporter, JacksonObjectMapper mapper) {
this.repository = repository;
this.assetRepository = assetRepository;
this.assetImporter = assetImporter;
this.mapper = mapper;
}
/**
......@@ -74,20 +76,27 @@ public class AssetService<T extends Assetable> {
checkArgument(file != null && !file.isEmpty(), "Part named [file] is needed to successfully import a component");
checkArgument(assetType != null, ASSET_TYPE_IS_REQUIRED);
final Asset asset = new Asset()
.setId(randomUUID().toString())
.setName(getOriginalFilename(file.getOriginalFilename()))
.setType(assetType)
.setOrder(getNextOrder(component));
deleteComponentAsset(component, new Predicate<Asset>() {
@Override
public boolean apply(Asset element) {
return asset.equalsWithoutComponentId(element);
try {
if (AssetType.JSON.getPrefix().equals(type)) {
checkWellFormedJson(file.getBytes());
}
});
try {
final Asset asset = new Asset()
.setId(randomUUID().toString())
.setName(getOriginalFilename(file.getOriginalFilename()))
.setType(assetType)
.setOrder(getNextOrder(component));
deleteComponentAsset(component, new Predicate<Asset>() {
@Override
public boolean apply(Asset element) {
return asset.equalsWithoutComponentId(element);
}
});
assetRepository.save(component.getId(), asset, file.getBytes());
//The component is updated
component.addAsset(asset);
......@@ -96,7 +105,16 @@ public class AssetService<T extends Assetable> {
} catch (IOException e) {
logger.error("Asset creation" + e);
throw new ServerImportException(String.format("Error while uploading asset in %s [%s]", file.getOriginalFilename(), repository.getComponentName()), e);
throw new ServerImportException(String.format("Error while uploading asset in %s [%s]", file.getOriginalFilename(), repository.getComponentName()),
e);
}
}
private void checkWellFormedJson(byte[] bytes) throws IOException {
try {
mapper.checkValidJson(bytes);
} catch (JsonProcessingException e) {
throw new MalformedJsonException(e);
}
}
......@@ -127,6 +145,7 @@ public class AssetService<T extends Assetable> {
if (asset.getId() != null) {
//We find the existing asset and change the name and the type
Iterables.<Asset>find(component.getAssets(), new Predicate<Asset>() {
@Override
public boolean apply(Asset element) {
return asset.getId().equals(element.getId());
......@@ -169,6 +188,7 @@ public class AssetService<T extends Assetable> {
checkArgument(isNotEmpty(assetId), ASSET_ID_IS_REQUIRED);
deleteComponentAsset(component, new Predicate<Asset>() {
@Override
public boolean apply(Asset asset) {
return assetId.equals(asset.getId());
......
/**
* 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.asset;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonProcessingException;
public class MalformedJsonException extends RuntimeException {
private Location location;
public MalformedJsonException(JsonProcessingException cause) {
super(cause);
this.location = new Location(cause.getLocation());
}
public Location getLocation() {
return location;
}
public static class Location {
private int line;
private int column;
public Location(JsonLocation location) {
this.line = location.getLineNr();
this.column = location.getColumnNr();
}
public int getLine() {
return line;
}
public void setLine(int line) {
this.line = line;
}
public int getColumn() {
return column;
}
public void setColumn(int column) {
this.column = column;
}
}
}
......@@ -17,6 +17,7 @@ package org.bonitasoft.web.designer.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
......@@ -51,4 +52,8 @@ public class JacksonObjectMapper {
public String prettyPrint(String json) throws IOException {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(fromJson(json.getBytes(StandardCharsets.UTF_8), Object.class));
}
public void checkValidJson(byte[] bytes) throws IOException, JsonProcessingException {
objectMapper.readTree(bytes);
}
}
/**
* 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 com.fasterxml.jackson.core;
/**
* Extends jackson JsonProcessingException for test purpose since JsonProcessingException constructor is protected
*/
public class FakeJsonProcessingException extends JsonProcessingException {
public FakeJsonProcessingException(String msg, byte[] srcRef, int line, int column) {
super(msg, new JsonLocation(srcRef, srcRef.length, line, column));
}
}
......@@ -41,7 +41,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.core.FakeJsonProcessingException;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.bonitasoft.web.designer.controller.asset.AssetService;
import org.bonitasoft.web.designer.controller.asset.MalformedJsonException;
import org.bonitasoft.web.designer.experimental.mapping.ContractToPageMapper;
import org.bonitasoft.web.designer.experimental.mapping.FormScope;
import org.bonitasoft.web.designer.model.asset.Asset;
......@@ -102,6 +105,13 @@ public class PageResourceTest {
mockMvc = mockServer(pageResource).build();
}
private Page mockPageOfId(String id) {
Page page = aPage().withId(id).build();
when(pageRepository.get(id)).thenReturn(page);
return page;
}
@Test
public void should_list_pages() throws Exception {
Page page = new Page();
......@@ -310,8 +320,7 @@ public class PageResourceTest {
@Test
public void should_rename_a_page() throws Exception {
String newName = "my-page-new-name";
Page page = aFilledPage("my-page");
when(pageRepository.get("my-page")).thenReturn(page);
mockPageOfId("my-page");
mockMvc
.perform(
......@@ -336,8 +345,7 @@ public class PageResourceTest {
@Test
public void should_respond_500_internal_error_if_error_occurs_while_renaming_a_page() throws Exception {
doThrow(new RepositoryException("exception occurs", new Exception())).when(pageRepository).updateLastUpdateAndSave(any(Page.class));
Page page = aFilledPage("my-page");
when(pageRepository.get("my-page")).thenReturn(page);
mockPageOfId("my-page");
mockMvc
.perform(put("/rest/pages/my-page/name").contentType(MediaType.APPLICATION_JSON_VALUE).content(convertObjectToJsonBytes("hello")))
......@@ -348,10 +356,9 @@ public class PageResourceTest {
public void should_upload_a_local_asset() throws Exception {
//We construct a mockfile (the first arg is the name of the property expected in the controller
MockMultipartFile file = new MockMultipartFile("file", "myfile.js", "application/javascript", "foo".getBytes());
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset expectedAsset = anAsset().withId("assetId").active().withName("myfile.js").withOrder(2).withScope(PAGE)
.withType(AssetType.JAVASCRIPT).build();
when(pageRepository.get("my-page")).thenReturn(page);
when(pageAssetService.upload(file, page, "js")).thenReturn(expectedAsset);
mockMvc.perform(fileUpload("/rest/pages/my-page/assets/js").file(file))
......@@ -365,12 +372,33 @@ public class PageResourceTest {
verify(pageAssetService).upload(file, page, "js");
}
@Test
public void should_respond_202_with_error_when_uploading_a_json_asset_with_malformed_json_file() throws Exception {
byte[] content = "notvalidjson".getBytes();
MockMultipartFile file = aJsonFileWithContent(content);
int expectedLine = 4, expectedColumn = 2;
when(pageAssetService.upload(file, mockPageOfId("my-page"), "json")).thenThrow(aMalformedJsonException(content, expectedLine, expectedColumn));
mockMvc.perform(fileUpload("/rest/pages/my-page/assets/json").file(file))
.andExpect(status().isAccepted())
.andExpect(jsonPath("$.type").value("MalformedJsonException"))
.andExpect(jsonPath("$.infos.location.line").value(expectedLine))
.andExpect(jsonPath("$.infos.location.column").value(expectedColumn));
}
private MockMultipartFile aJsonFileWithContent(byte[] content) {
return new MockMultipartFile("file", "myfile.js", "application/json", content);
}
private MalformedJsonException aMalformedJsonException(byte[] bytes, int errorLine, int errorColumn) {
return new MalformedJsonException(new FakeJsonProcessingException("Error while checking json", bytes, errorLine, errorColumn));
}
@Test
public void should_not_upload_an_asset_when_upload_send_an_error() throws Exception {
//We construct a mockfile (the first arg is the name of the property expected in the controller
MockMultipartFile file = new MockMultipartFile("file", "myfile.js", "application/javascript", "foo".getBytes());
Page page = aFilledPage("my-page");
when(pageRepository.get("my-page")).thenReturn(page);
Page page = mockPageOfId("my-page");
doThrow(IllegalArgumentException.class).when(pageAssetService).upload(file, page, "js");
mockMvc.perform(fileUpload("/rest/pages/my-page/assets/js").file(file))
......@@ -381,10 +409,9 @@ public class PageResourceTest {
@Test
public void should_save_an_external_asset() throws Exception {
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset expectedAsset = anAsset().withId("assetId").active().withName("myfile.js").withOrder(2).withScope(PAGE)
.withType(AssetType.JAVASCRIPT).build();
when(pageRepository.get("my-page")).thenReturn(page);
when(pageAssetService.save(page, expectedAsset)).thenReturn(expectedAsset);
mockMvc.perform(
......@@ -403,9 +430,8 @@ public class PageResourceTest {
@Test
public void should_not_save_an_external_asset_when_upload_send_an_error() throws Exception {
Page page = aFilledPage("my-page");
Page page = mockPageOfId("my-page");
Asset asset = anAsset().build();
when(pageRepository.get("my-page")).thenReturn(page);
doThrow(IllegalArgumentException.class).when(pageAssetService).save(page, asset);
mockMvc.perform(
......@@ -417,14 +443,12 @@ public class PageResourceTest {
@Test
public void should_list_page_assets() throws Exception {
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset[] assets = new Asset[] {
anAsset().withName("myCss.css").withType(AssetType.CSS).withScope(AssetScope.WIDGET).withComponentId("widget-id").build(),
anAsset().withName("myJs.js").withType(JAVASCRIPT).withScope(AssetScope.PAGE).build(),
anAsset().withName("https://mycdn.com/myExternalJs.js").withScope(AssetScope.PAGE).withType(JAVASCRIPT).build()
};
when(pageRepository.get("my-page")).thenReturn(page);
when(assetVisitor.visit(page)).thenReturn(Sets.newHashSet(assets));
mockMvc.perform(get("/rest/pages/my-page/assets"))
......@@ -439,14 +463,12 @@ public class PageResourceTest {
@Test
public void should_list_page_assets_while_getting_a_page() throws Exception {
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset[] assets = new Asset[] {
anAsset().withName("myCss.css").withType(AssetType.CSS).withScope(AssetScope.WIDGET).withComponentId("widget-id").build(),
anAsset().withName("myJs.js").withType(AssetType.JAVASCRIPT).withScope(AssetScope.PAGE).build(),
anAsset().withName("https://mycdn.com/myExternalJs.js").withType(AssetType.JAVASCRIPT).withScope(AssetScope.PAGE).build()
};
when(pageRepository.get("my-page")).thenReturn(page);
when(assetVisitor.visit(page)).thenReturn(Sets.newHashSet(assets));
mockMvc.perform(get("/rest/pages/my-page"))
......@@ -460,9 +482,8 @@ public class PageResourceTest {
@Test
public void should_increment_an_asset() throws Exception {
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset asset = anAsset().withComponentId("my-page").withOrder(3).build();
when(pageRepository.get("my-page")).thenReturn(page);
mockMvc.perform(
put("/rest/pages/my-page/assets/UIID?increment=true")
......@@ -475,9 +496,8 @@ public class PageResourceTest {
@Test
public void should_decrement_an_asset() throws Exception {
Page page = aPage().withId("my-page").build();
Page page = mockPageOfId("my-page");
Asset asset = anAsset().withComponentId("my-page").withOrder(3).build();
when(pageRepository.get("my-page")).thenReturn(page);
mockMvc.perform(
put("/rest/pages/my-page/assets/UIID?decrement=true")
......@@ -490,9 +510,7 @@ public class PageResourceTest {
@Test
public void should_delete_an_asset() throws Exception {
Page page = aFilledPage("my-page");
Asset asset = anAsset().withComponentId("my-page").build();
when(pageRepository.get("my-page")).thenReturn(page);
Page page = mockPageOfId("my-page");
mockMvc.perform(
delete("/rest/pages/my-page/assets/UIID")
......@@ -504,9 +522,8 @@ public class PageResourceTest {
@Test
public void should_inactive_an_asset() throws Exception {
Page page = aPage().withId("my-page").build();
Asset asset = anAsset().withComponentId("my-page").withOrder(3).build();
when(pageRepository.get("my-page")).thenReturn(page);
Page page = mockPageOfId("my-page");
Asset asset = anAsset().withComponentId(page.getId()).withOrder(3).build();
mockMvc.perform(
put("/rest/pages/my-page/assets/UIID?active=false")
......
......@@ -35,6 +35,7 @@ import java.util.List;
import com.google.common.collect.Lists;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.bonitasoft.web.designer.config.DesignerConfig;
import org.bonitasoft.web.designer.controller.importer.ServerImportException;
import org.bonitasoft.web.designer.controller.importer.dependencies.AssetImporter;
import org.bonitasoft.web.designer.model.asset.Asset;
......@@ -43,11 +44,11 @@ import org.bonitasoft.web.designer.model.page.Page;
import org.bonitasoft.web.designer.repository.AssetRepository;
import org.bonitasoft.web.designer.repository.Repository;
import org.bonitasoft.web.designer.repository.exception.RepositoryException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
......@@ -57,18 +58,24 @@ import org.springframework.mock.web.MockMultipartFile;
@RunWith(JUnitParamsRunner.class)
public class AssetServiceTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
private Repository<Page> repository;
@Mock
private AssetRepository<Page> assetRepository;
@Mock
private AssetImporter<Page> assetImporter;
@InjectMocks
private AssetService assetService;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Before
public void setUp() throws Exception {
assetService = new AssetService(repository, assetRepository, assetImporter, new DesignerConfig().objectMapperWrapper());
}
@Test
public void should_return_error_when_uploading_file_null() {
......@@ -143,6 +150,13 @@ public class AssetServiceTest {
verify(repository).updateLastUpdateAndSave(page);
}
@Test(expected = MalformedJsonException.class)
public void should_check_that_json_is_well_formed_while_uploading_a_json_asset() throws Exception {
MockMultipartFile file = new MockMultipartFile("asset.json", "asset.json", "application/javascript", "{ not json }".getBytes());
assetService.upload(file, aPage().build(), "json");
}
@Test
public void should_return_error_when_adding_asset_with_name_null() {
expectedException.expect(IllegalArgumentException.class);
......
......@@ -18,8 +18,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.bonitasoft.web.designer.config.DesignerConfig;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runners.JUnit4;
import org.skyscreamer.jsonassert.JSONAssert;
public class JacksonObjectMapperTest {
......@@ -67,4 +70,15 @@ public class JacksonObjectMapperTest {
" \"foo\" : \"bar\"" + System.lineSeparator() +
"}");
}
@Test(expected = JsonProcessingException.class)
public void should_check_that_json_is_invalid() throws Exception {
objectMapper.checkValidJson("{ not json }".getBytes());
}
@Test
public void should_check_that_json_is_valid() throws Exception {
objectMapper.checkValidJson("{ \"collection\": [\n] }".getBytes());
// ok - no exception expected
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment