Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Commit

Permalink
Update project metadata through REST API
Browse files Browse the repository at this point in the history
This commit adds a new PATCH method on the "/project_metadata/{project}"
route that patches the project metadata information.

This is useful for automated project release processes.

Closes gh-888
Closes gh-889
  • Loading branch information
marcingrzejszczak authored and bclozel committed Feb 25, 2019
1 parent 290ce6e commit f966492
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 30 deletions.
32 changes: 27 additions & 5 deletions sagan-common/src/main/java/sagan/projects/Project.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
package sagan.projects;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.NamedAttributeNode;
import javax.persistence.NamedEntityGraph;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;

import javax.persistence.*;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.util.StringUtils;

@Entity
@NamedEntityGraph(name = "Project.tree",
attributeNodes = @NamedAttributeNode("childProjectList"))
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Project {

@Id
Expand Down Expand Up @@ -105,6 +123,9 @@ public String getId() {
* @return The list of releases sorted in descending order by version
*/
public List<ProjectRelease> getProjectReleases() {
if (releaseList == null) {
return new ArrayList<>();
}
releaseList.sort(Collections.reverseOrder(ProjectRelease::compareTo));
return releaseList;
}
Expand All @@ -126,7 +147,8 @@ public String getStackOverflowTags() {
}

public void setStackOverflowTags(String stackOverflowTags) {
this.stackOverflowTags = stackOverflowTags.replaceAll(" ", "");
this.stackOverflowTags = stackOverflowTags != null ?
stackOverflowTags.replaceAll(" ", "") : "";
}

public Set<String> getStackOverflowTagList() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sagan.projects;

import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.springframework.stereotype.Service;

@Service
public class ProjectPatchingService {

public Project patch(Project newValue, Project valueToMutate) {
return ObjectPatcher.patch(newValue, valueToMutate)
.mutateIfDirty(Project::getRawOverview, Project::setRawOverview)
.mutateIfDirty(Project::getRawBootConfig, Project::setRawBootConfig)
.patchedValue();
}

// could be reused in other projects
static class ObjectPatcher<T> {
private T newValue;

private T valueToMutate;

private ObjectPatcher(T newValue, T valueToMutate) {
this.newValue = newValue;
this.valueToMutate = valueToMutate;
}

static <T> ObjectPatcher<T> patch(T newValue, T valueToMutate) {
return new ObjectPatcher<>(newValue, valueToMutate);
}

<V> ObjectPatcher<T> mutateIfDirty(Function<T, V> getter, BiConsumer<T, V> modifyIfDirty) {
V newValue = getter.apply(this.newValue);
V valueToMutate = getter.apply(this.valueToMutate);
if (isDirty(newValue, valueToMutate)) {
// mutates the old object
modifyIfDirty.accept(this.valueToMutate, newValue);
}
return this;
}

// syntactic sugar since new value got mutated all the way
T patchedValue() {
return this.valueToMutate;
}

private boolean isDirty(Object newValue, Object oldValue) {
return newValue != null && !Objects.equals(newValue, oldValue);
}
}
}


Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package sagan.projects.support;

import sagan.projects.Project;
import sagan.projects.ProjectRelease;
import sagan.projects.ProjectRelease.ReleaseStatus;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -13,7 +9,14 @@

import javax.persistence.EntityManager;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.assertj.core.api.BDDAssertions;
import org.junit.Test;
import sagan.projects.Project;
import sagan.projects.ProjectRelease;
import sagan.projects.ProjectRelease.ReleaseStatus;
import saganx.AbstractIntegrationTests;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.json.JacksonJsonParser;
Expand All @@ -23,19 +26,20 @@
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.client.WireMock;

import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import saganx.AbstractIntegrationTests;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureRestDocs(outputDir = "build/snippets")
public class ProjectsMetadataApiTests extends AbstractIntegrationTests {
Expand Down Expand Up @@ -167,6 +171,28 @@ public void projectMetadata_updateProject() throws Exception {
entityManager.flush();
}

@Test
public void projectMetadata_patchProject() throws Exception {
Project project = new Project("spring-framework", null, null, null, null, null);
project.setRawBootConfig("rawBootConfig");
project.setRawOverview("rawOverview");

mockMvc
.perform(
MockMvcRequestBuilders
.patch("/project_metadata/spring-framework")
.content(mapper.writeValueAsString(project))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("application/json"))
.andDo(docs("patch_project"));
entityManager.flush();

Project storedProject = entityManager.find(Project.class, "spring-framework");
BDDAssertions.then(storedProject.getRawBootConfig()).isEqualTo("rawBootConfig");
BDDAssertions.then(storedProject.getRawOverview()).isEqualTo("rawOverview");
}

private Map<String, Object> getRelease(ProjectRelease release) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("groupId", release.getGroupId());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package sagan.projects.support;

import sagan.projects.Project;
import sagan.projects.ProjectRelease;
import sagan.support.JsonPController;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import sagan.projects.Project;
import sagan.projects.ProjectPatchingService;
import sagan.projects.ProjectRelease;
import sagan.support.JsonPController;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -16,7 +17,12 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;

import static org.springframework.web.bind.annotation.RequestMethod.*;
import static org.springframework.web.bind.annotation.RequestMethod.DELETE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
import static org.springframework.web.bind.annotation.RequestMethod.PATCH;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import static org.springframework.web.bind.annotation.RequestMethod.PUT;

/**
* Controller that handles ajax requests for project metadata, typically from the
Expand All @@ -29,10 +35,13 @@
class ProjectMetadataController {

private final ProjectMetadataService service;
private final ProjectPatchingService projectPatchingService;

@Autowired
public ProjectMetadataController(ProjectMetadataService service) {
public ProjectMetadataController(ProjectMetadataService service,
ProjectPatchingService projectPatchingService) {
this.service = service;
this.projectPatchingService = projectPatchingService;
}

@RequestMapping(value = "/{projectId}", method = { GET, HEAD })
Expand Down Expand Up @@ -109,6 +118,17 @@ public ProjectRelease removeReleaseMetadata(@PathVariable("projectId") String pr
return found;
}

@RequestMapping(value = "/{projectId}", method = PATCH)
public Project updateProject(@PathVariable("projectId") String projectId,
@RequestBody Project projectWithPatches) throws IOException {
Project project = service.getProject(projectId);
if (project == null) {
throw new MetadataNotFoundException("Cannot find project " + projectId);
}
Project patchedProject = projectPatchingService.patch(projectWithPatches, project);
return service.save(patchedProject);
}

@ExceptionHandler(MetadataNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public void handle() {
Expand All @@ -121,5 +141,4 @@ public MetadataNotFoundException(String string) {
}

}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package sagan.projects.support;

import sagan.projects.Project;
import sagan.projects.ProjectRelease;
import sagan.projects.ProjectRelease.ReleaseStatus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -12,7 +8,12 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import sagan.projects.Project;
import sagan.projects.ProjectPatchingService;
import sagan.projects.ProjectRelease;
import sagan.projects.ProjectRelease.ReleaseStatus;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
Expand All @@ -36,7 +37,7 @@ public class ProjectMetadataControllerTests {

@Before
public void setUp() throws Exception {
controller = new ProjectMetadataController(projectMetadataService);
controller = new ProjectMetadataController(projectMetadataService, new ProjectPatchingService());
}

@Test
Expand Down Expand Up @@ -78,4 +79,25 @@ public void addProjectRelease_replacesVersionPatterns() throws Exception {
"http://example.com/1.2.4"));
}

@Test
public void updateProject_patchesTheProject() throws Exception {
List<ProjectRelease> newReleases = new ArrayList<>();
newReleases.add(new ProjectRelease("1.0.0.RELEASE", ReleaseStatus.GENERAL_AVAILABILITY, true, "foo", "bar", "com.example", "artifact"));
Project newProject = new Project(PROJECT_ID, null, "http://example.com", "newSite", newReleases, "newProject");
newProject.setRawBootConfig("newRawBootConfig");
newProject.setRawOverview("newRawOverview");
when(projectMetadataService.getProject(PROJECT_ID)).thenReturn(project);
when(projectMetadataService.save(Mockito.anyObject()))
.thenAnswer(invocation -> invocation.getArguments()[0]);

Project updatedProject = controller.updateProject(PROJECT_ID, newProject);

// for now we only patch the raw stuff
assertThat(updatedProject.getName(), equalTo(project.getName()));
assertThat(updatedProject.getRepoUrl(), equalTo(project.getRepoUrl()));

assertThat(updatedProject.getRawOverview(), equalTo("newRawOverview"));
assertThat(updatedProject.getRawBootConfig(), equalTo("newRawBootConfig"));
}

}

0 comments on commit f966492

Please sign in to comment.