Commit 88ed31ba authored by Matija Obreza's avatar Matija Obreza

Merge branch '350-crops-api-v1' into 'master'

Resolve "Crops API v1"

Closes #350

See merge request genesys-pgr/genesys-server!262
parents 7f566b0c 2d051270
/**
* Copyright 2014 Global Crop Diversity Trust
*
* 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.
**/
package org.genesys2.server.api.v1;
import java.util.List;
import javax.xml.bind.ValidationException;
import org.genesys.blocks.model.JsonViews;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys2.server.api.ApiBaseController;
import org.genesys2.server.api.ModelValidationException;
import org.genesys2.server.api.OAuth2Cleanup;
import org.genesys2.server.exception.AuthorizationException;
import org.genesys2.server.model.genesys.Parameter;
import org.genesys2.server.model.impl.Crop;
import org.genesys2.server.model.impl.CropRule;
import org.genesys2.server.model.impl.CropTaxonomy;
import org.genesys2.server.service.CropService;
import org.genesys2.server.service.GenesysService;
import org.genesys2.server.service.TraitService;
import org.genesys2.spring.ResourceNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.annotations.Api;
import net.sf.oval.ConstraintViolation;
import net.sf.oval.Validator;
@RestController("cropApi1")
@RequestMapping(value = { CropsController.CONTROLLER_URL, "/json/v1/crops" })
@Api(tags = { "crop" })
public class CropsController extends ApiBaseController {
public static final String CONTROLLER_URL = ApiBaseController.APIv1_BASE + "/crops";
@Autowired
GenesysService genesysService;
@Autowired
TraitService traitService;
@Autowired
CropService cropService;
/**
* List all crops
* @return
*
* @return
* @throws AuthorizationException
*/
@RequestMapping(value = "", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public List<CropService.CropDetails> listCrops() {
LOG.info("Listing crops");
return cropService.listDetails(LocaleContextHolder.getLocale());
}
/**
* Add a crop
* @return
*
* @return
* @throws ValidationException
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
@RequestMapping(value = { "/save" }, method = { RequestMethod.PUT, RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public Crop saveCrop(@RequestBody Crop cropJson) throws ValidationException {
LOG.info("Creating crop");
final Validator validator = new Validator();
final List<ConstraintViolation> violations = validator.validate(cropJson);
if (violations.size() > 0) {
throw new ModelValidationException("Crop does not validate", violations);
}
Crop crop = cropService.getCrop(cropJson.getShortName());
if (crop == null) {
crop = cropService.addCrop(cropJson);
} else {
crop = cropService.updateCrop(cropJson);
}
return crop;
}
/**
* Get crop /crops/{shortName}
* @return
*
* @return
* @throws AuthorizationException
*/
@RequestMapping(value = "/{shortName}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Crop getCrop(@PathVariable("shortName") String shortName) throws AuthorizationException {
LOG.info("Getting crop {}", shortName);
return cropService.getCrop(shortName);
}
/**
* Get crop details /crops/{shortName}/details
* @return
*
* @return
* @throws AuthorizationException
*/
@JsonView(JsonViews.Public.class)
@RequestMapping(value = "/{shortName}/details", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public CropService.CropDetails getCropDetails(@PathVariable("shortName") String shortName) throws InvalidRepositoryPathException {
LOG.info("Getting crop details {}", shortName);
return cropService.getDetails(shortName, LocaleContextHolder.getLocale());
}
/**
* Delete crop /crops/{shortName}
*
* @return
* @throws AuthorizationException
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
@RequestMapping(value = "/{shortName}", method = RequestMethod.DELETE, produces = { MediaType.APPLICATION_JSON_VALUE })
public @ResponseBody Crop deleteCrop(@PathVariable("shortName") String shortName) throws AuthorizationException {
LOG.info("Getting crop {}", shortName);
return cropService.delete(cropService.getCrop(shortName));
}
/**
* Get crop descriptors /crops/{shortName}/descriptors
* @return
*
* @return
* @throws AuthorizationException
*/
@RequestMapping(value = "/{shortName}/descriptors", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Page<Parameter> getCropDescriptors(@PathVariable("shortName") String shortName) throws AuthorizationException {
LOG.info("Getting crop descriptors {}", shortName);
final Crop crop = cropService.getCrop(shortName);
final Page<Parameter> descriptors = traitService.listTraits(crop, new PageRequest(0, 50));
return OAuth2Cleanup.clean(descriptors);
}
/**
* Get crop taxonomy rules /crops/{shortName}/rules
* @return
*
* @return
* @throws AuthorizationException
*/
@RequestMapping(value = "/{shortName}/rules", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public List<CropRule> getCropRules(@PathVariable("shortName") String shortName) throws AuthorizationException {
LOG.info("Getting crop rules {}", shortName);
final Crop crop = cropService.getCrop(shortName);
final List<CropRule> cropRules = cropService.getCropRules(crop);
return cropRules;
}
/**
* Get crop taxonomy rules /crops/{shortName}/rules
*
* @return
*
* @return
* @throws AuthorizationException
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
@RequestMapping(value = "/{shortName}/rules", method = { RequestMethod.PUT, RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public @ResponseBody List<CropRule> updateCropRules(@PathVariable("shortName") String shortName, @RequestBody List<CropRule> rules) throws AuthorizationException {
LOG.info("Updating crop rules for {}", shortName);
final Crop crop = cropService.getCrop(shortName);
if (crop == null)
throw new ResourceNotFoundException("No crop " + shortName);
cropService.setCropRules(crop, rules);
return cropService.getCropRules(crop);
}
/**
* Get crop taxonomies /crops/{shortName}/taxa
* @return
*
* @return
* @throws AuthorizationException
*/
@RequestMapping(value = "/{shortName}/taxa", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public Page<CropTaxonomy> getCropTaxa(@PathVariable("shortName") String shortName) throws AuthorizationException {
LOG.info("Getting crop taxa {}", shortName);
final Crop crop = cropService.getCrop(shortName);
final Page<CropTaxonomy> cropTaxa = cropService.getCropTaxonomies(crop, new PageRequest(0, 50));
return OAuth2Cleanup.clean(cropTaxa);
}
/**
* Rebuild taxonomy-crop lists
*
* @return
* @throws AuthorizationException
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
@RequestMapping(value = "/rebuild", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public void rebuild() {
cropService.rebuildTaxonomies();
}
/**
* Rebuild taxonomy-crop lists
*
* @return
* @throws AuthorizationException
*/
@PreAuthorize("hasRole('ADMINISTRATOR')")
@RequestMapping(value = "{shortName}/rebuild", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
public void rebuildCrop(@PathVariable("shortName") String shortName) {
LOG.info("Updating crop rules for {}", shortName);
final Crop crop = cropService.getCrop(shortName);
if (crop == null)
throw new ResourceNotFoundException("No crop " + shortName);
cropService.rebuildTaxonomies(crop);
}
}
......@@ -187,12 +187,8 @@ public class PermissionController {
*
* @param term the term
* @return the map
* @deprecated Will be deprecated in 1.7
*/
@Deprecated
// TODO remove
@GetMapping(value = "/autocompleteuser", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> acUser(@RequestParam("term") final String term) {
private Map<String, Long> acUser(@RequestParam("term") final String term) {
final Map<String, Long> userIds = new HashMap<>();
for (final User user : userService.autocompleteUser(term, 10)) {
userIds.put(user.getEmail(), user.getId());
......@@ -205,12 +201,8 @@ public class PermissionController {
*
* @param term the term
* @return the map
* @deprecated Will be deprecated in 1.7
*/
@Deprecated
// TODO remove
@GetMapping(value = "/autocompleterole", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> acRole(@RequestParam("term") final String term) {
private Map<String, Long> acRole(@RequestParam("term") final String term) {
final Map<String, Long> roleSids = new HashMap<>();
final List<UserRole> matchingRoles = Arrays.stream(UserRole.values()).filter(role -> role.name().toLowerCase().startsWith(term.toLowerCase())).collect(Collectors.toList());
......@@ -227,12 +219,8 @@ public class PermissionController {
*
* @param term the term
* @return the map
* @deprecated Will be deprecated in 1.7
*/
@Deprecated
// TODO remove
@GetMapping(value = "/autocomplete-oauth-client", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> acOauthClient(@RequestParam("term") final String term) {
private Map<String, Long> acOauthClient(@RequestParam("term") final String term) {
final Map<String, Long> oauthMap = new HashMap<>();
for (final OAuthClient client : clientDetailsService.autocompleteClients(term, 10)) {
oauthMap.put(client.getTitle(), client.getId());
......
......@@ -80,6 +80,9 @@ public class Crop extends GlobalVersionedAuditedModel implements AclAwareModel {
@Column(nullable = false, length = 200)
private String name;
@Column(name = "annex1")
private boolean annex1 = false;
@Lob
@Type(type = "org.hibernate.type.TextType")
private String description;
......@@ -255,4 +258,12 @@ public class Crop extends GlobalVersionedAuditedModel implements AclAwareModel {
public void setOtherNames(List<String> otherNames) {
this.otherNames = otherNames;
}
public boolean isAnnex1() {
return annex1;
}
public void setAnnex1(boolean annex1) {
this.annex1 = annex1;
}
}
......@@ -16,20 +16,33 @@
package org.genesys2.server.service;
import java.io.Serializable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.genesys.blocks.model.JsonViews;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.RepositoryImage;
import org.genesys2.server.model.genesys.Taxonomy2;
import org.genesys2.server.model.impl.Article;
import org.genesys2.server.model.impl.Crop;
import org.genesys2.server.model.impl.CropRule;
import org.genesys2.server.model.impl.CropTaxonomy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.annotation.JsonView;
public interface CropService {
Crop getCrop(String shortName);
CropDetails getDetails(String shortName, Locale locale) throws InvalidRepositoryPathException;
List<CropDetails> listDetails(Locale locale);
List<Crop> listCrops();
Page<Crop> listCrops(Pageable pageable);
......@@ -57,6 +70,8 @@ public interface CropService {
*/
Crop addCrop(String shortName, String name, String description, String i18n);
Crop addCrop(Crop crop);
/**
* Updates a crop record
*
......@@ -68,6 +83,8 @@ public interface CropService {
*/
Crop updateCrop(Crop crop, String name, String description, String i18n);
Crop updateCrop(Crop crop);
CropRule addCropRule(Crop crop, String genus, String species, boolean included);
List<CropRule> getCropRules(Crop crop);
......@@ -109,5 +126,28 @@ public interface CropService {
*/
Crop updateAliases(Crop crop, List<String> otherNames);
public static class CropDetails implements Serializable {
private static final long serialVersionUID = 1622015024393351713L;
@JsonUnwrapped
public Crop crop;
@JsonView({ JsonViews.Public.class })
public Article blurb;
public long accessionCount;
public List<RepositoryImage> covers;
@JsonView({ JsonViews.Public.class })
public Map<String, ElasticsearchService.TermResult> overview;
public static CropDetails from(Crop crop, Article blurb, long accessionCount, List<RepositoryImage> covers, Map<String, ElasticsearchService.TermResult> overview) {
CropDetails cropDetails = new CropDetails();
cropDetails.crop = crop;
cropDetails.blurb = blurb;
cropDetails.accessionCount = accessionCount;
cropDetails.covers = covers;
cropDetails.overview = overview;
return cropDetails;
}
}
}
......@@ -15,6 +15,7 @@
*/
package org.genesys2.server.service.filter;
import java.io.Serializable;
import java.util.Set;
import java.util.UUID;
......@@ -32,7 +33,7 @@ import com.querydsl.core.types.Predicate;
/**
* Filters for {@link Accession}.
*/
public class AccessionFilter extends UuidModelFilter<AccessionFilter, Accession> {
public class AccessionFilter extends UuidModelFilter<AccessionFilter, Accession> implements Serializable {
/** The historic. */
public Boolean historic;
......
......@@ -150,6 +150,7 @@ public class AccessionServiceImpl implements AccessionService {
}
@Override
@Cacheable(value = "accessionCount", keyGenerator = "shortFilterKeyGenerator")
public long countAccessions(AccessionFilter filter) {
long total = elasticsearchService.count(Accession.class, filter);
......
......@@ -16,19 +16,27 @@
package org.genesys2.server.service.impl;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.RepositoryImage;
import org.genesys.filerepository.service.ImageGalleryService;
import org.genesys2.server.model.genesys.Accession;
import org.genesys2.server.model.genesys.Taxonomy2;
import org.genesys2.server.model.impl.Article;
import org.genesys2.server.model.impl.Crop;
import org.genesys2.server.model.impl.CropRule;
import org.genesys2.server.model.impl.CropTaxonomy;
......@@ -36,10 +44,13 @@ import org.genesys2.server.persistence.CropRepository;
import org.genesys2.server.persistence.CropRuleRepository;
import org.genesys2.server.persistence.CropTaxonomyRepository;
import org.genesys2.server.persistence.Taxonomy2Repository;
import org.genesys2.server.service.AccessionService;
import org.genesys2.server.service.CRMException;
import org.genesys2.server.service.ContentService;
import org.genesys2.server.service.CropService;
import org.genesys2.server.service.ElasticsearchService;
import org.genesys2.server.service.HtmlSanitizer;
import org.genesys2.server.service.filter.AccessionFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -76,6 +87,15 @@ public class CropServiceImpl implements CropService {
@Autowired
private ContentService contentService;
@Autowired
private AccessionService accessionService;
@Autowired
private ElasticsearchService elasticsearchService;
@Autowired
private ImageGalleryService imageGalleryService;
@Autowired
private Taxonomy2Repository taxonomy2Repository;
......@@ -100,6 +120,24 @@ public class CropServiceImpl implements CropService {
return crop;
}
@Override
@Transactional
public CropDetails getDetails(String shortName, Locale locale) throws InvalidRepositoryPathException {
Crop crop = getCrop(shortName);
Article article = contentService.getArticle(crop, "blurp", locale);
AccessionFilter byCrop = new AccessionFilter();
byCrop.cropName = crop.getShortName();
long accessionCount = accessionService.countAccessions(byCrop);
ImageGallery imageGallery = imageGalleryService.loadImageGallery(Paths.get("/crop", crop.getShortName(), "covers"));
List<RepositoryImage> covers = imageGallery == null ? Collections.emptyList() : imageGallery.getImages();
Map<String, ElasticsearchService.TermResult> overview = getOverviewData(byCrop);
return CropDetails.from(crop, article, accessionCount, covers, overview);
}
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Override
@Transactional
......@@ -128,9 +166,9 @@ public class CropServiceImpl implements CropService {
return cropRepository.save(crop);
}
@Override
@Cacheable(value = CACHE_CROPS, key = "'findAll'")
@Cacheable(value = CACHE_CROPS, key = "'listCrops'")
public List<Crop> listCrops() {
List<Crop> crops = cropRepository.findAll();
// Fetch otherNames list
......@@ -141,6 +179,31 @@ public class CropServiceImpl implements CropService {
return crops;
}
@Override
@Cacheable(value = CACHE_CROPS, key = "'findAll-' + #locale.language")
public List<CropDetails> listDetails(Locale locale) {
List<Crop> crops = cropRepository.findAll();
// Fetch details
return crops.stream().map(c -> {
if (c.getOtherNames() != null)
c.getOtherNames().size();
AccessionFilter byCrop = new AccessionFilter();
byCrop.cropName = c.getShortName();
long accessionCount = accessionService.countAccessions(byCrop);
List<RepositoryImage> covers = null;
try {
ImageGallery imageGallery = imageGalleryService.loadImageGallery(Paths.get("/crop", c.getShortName(), "covers"));
covers = imageGallery == null ? Collections.emptyList() : imageGallery.getImages();
} catch (InvalidRepositoryPathException e) {
}
return CropDetails.from(c, null, accessionCount, covers, null);
}).collect(Collectors.toList());
}
@Override
public Page<Crop> listCrops(Pageable pageable) {
return cropRepository.findAll(pageable);
......@@ -346,6 +409,24 @@ public class CropServiceImpl implements CropService {
return crop;
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#crop, 'ADMINISTRATION')")
@Transactional(readOnly = false)
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop updateCrop(Crop crop) {
final Crop toUpdate = getCrop(crop.getShortName());
copyValues(toUpdate, crop);
return cropRepository.save(toUpdate);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional(readOnly = false)
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop addCrop(Crop crop) {
return cropRepository.save(crop);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#crop, 'ADMINISTRATION')")
@Transactional(readOnly = false)
......@@ -445,4 +526,30 @@ public class CropServiceImpl implements CropService {
cropTaxonomyRepository.save(toAdd);
}
}
private Map<String, ElasticsearchService.TermResult> getOverviewData(AccessionFilter byCropFilter) {
String[] terms = new String[] {"taxonomy.genus", "taxonomy.genusSpecies", "institute.code",
"institute.country.code3", "mlsStatus", "available"};
try {
return elasticsearchService.termStatisticsAuto(Accession.class, byCropFilter, 10, terms);
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
return null;
}
}
private void copyValues(Crop target, Crop source) {
target.setDescription(source.getDescription());
target.setI18n(source.getI18n());
target.setName(source.getName());
if (source.getOtherNames() != null) {
target.setOtherNames(source.getOtherNames().stream().distinct().map(otherName -> StringUtils.trim(otherName)).filter(otherName -> StringUtils.isNotBlank(otherName)).sorted().collect(Collectors.toList()));
}
if (source.getCropRules() != null)
target.getCropRules().addAll(source.getCropRules());
}
}
......@@ -86,6 +86,7 @@ public class ApplicationUpgrades implements InitializingBean {
for (RepositoryFolder repositoryFolder : repositoryService.getFolders(Paths.get("/"))) {
LOG.warn("Making {} publicly readable", repositoryFolder.getPath());
aclService.addCreatorPermissions(repositoryFolder);
aclService.makePubliclyReadable(repositoryFolder, true);
repositoryService.listPaths(repositoryFolder.getFolderPath()).forEach(subFolder -> {
aclService.addCreatorPermissions(subFolder);
......
......@@ -4373,6 +4373,19 @@ databaseChangeLog:
indexName: IX_id3