Commit b7319463 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '117-languages-api-endpoint' into 'master'

Languages API endpoint

Closes #117

See merge request !109
parents e8a8aa73 86155f72
......@@ -31,6 +31,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
*/
public interface PartnerService {
/**
* Gets the primary partner.
*
* @return the primary partner
*/
Partner getPrimaryPartner();
/**
* Method creating Partner.
*
......
......@@ -21,6 +21,7 @@ import org.genesys.catalog.model.vocab.ControlledVocabulary;
import org.genesys.catalog.service.filters.ControlledVocabularyFilter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* The VocabularyService.
......@@ -58,6 +59,7 @@ public interface VocabularyService {
* @param input updated vocabulary definition
* @return persisted vocabulary
*/
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input, 'write')")
ControlledVocabulary updateVocabulary(ControlledVocabulary input);
/**
......@@ -66,6 +68,7 @@ public interface VocabularyService {
* @param vocabulary the vocabulary
* @return the controlled vocabulary
*/
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input, 'delete')")
ControlledVocabulary deleteVocabulary(ControlledVocabulary vocabulary);
/**
......
......@@ -25,7 +25,9 @@ import org.genesys.common.model.Partner;
import org.genesys.common.persistence.PartnerRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.domain.Page;
......@@ -40,7 +42,7 @@ import org.springframework.transaction.annotation.Transactional;
*/
@Service
@Transactional(readOnly = true)
public class PartnerServiceImpl implements PartnerService {
public class PartnerServiceImpl implements PartnerService, InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(PartnerServiceImpl.class);
......@@ -50,6 +52,30 @@ public class PartnerServiceImpl implements PartnerService {
@Autowired
private Utils utils;
@Value("${partner.primary.uuid}")
private UUID primaryPartnerUuid;
private Partner primaryPartner;
@Override
public void afterPropertiesSet() throws Exception {
Partner primaryPartner = loadPartner(primaryPartnerUuid);
if (primaryPartner == null) {
primaryPartner=new Partner();
primaryPartner.setUuid(primaryPartnerUuid);
primaryPartner.setName("Genesys Catalog");
primaryPartner.setShortName("genesys");
primaryPartner = createPartner(primaryPartner);
}
this.primaryPartner = primaryPartner;
}
@Override
public Partner getPrimaryPartner() {
return primaryPartner;
}
/**
* {@inheritDoc}
*/
......@@ -187,4 +213,5 @@ public class PartnerServiceImpl implements PartnerService {
}
return partner;
}
}
......@@ -66,6 +66,13 @@ public class UserServiceImpl extends BasicUserServiceImpl<UserRole, User> implem
public User deepLoad(User user) {
return super.deepLoad(user);
}
@Override
protected User createSystemAdministrator(String username) throws UserException {
User admin = createUser(username, "System Administrator", null, AccountType.SYSTEM);
setRoles(admin, Sets.newHashSet(UserRole.ADMINISTRATOR));
return admin;
}
@Override
public User getUserByEmail(String email) {
......
......@@ -39,7 +39,7 @@ import org.springframework.stereotype.Component;
*/
@Component
public class GeonamesISOLanguageSource {
private static final String GEONAMES_ISO639_URL = "http://download.geonames.org/export/dump/iso-languagecodes.txt";
public static final String GEONAMES_ISO639_URL = "http://download.geonames.org/export/dump/iso-languagecodes.txt";
/** The Constant LOG. */
public static final Logger LOG = LoggerFactory.getLogger(GeonamesISOLanguageSource.class);
......
......@@ -21,6 +21,7 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import org.apache.commons.logging.Log;
......@@ -39,56 +40,63 @@ import org.springframework.stereotype.Component;
@Component
public class ISO639VocabularyUpdater {
/** The Constant LOG. */
public static final Log LOG = LogFactory.getLog(ISO639VocabularyUpdater.class);
/**
* ISO 639 representation of names for languages and language groups.
* https://en.wikipedia.org/wiki/ISO_639
*/
public static UUID ISO639_3 = UUID.fromString("21b10067-ba15-44dd-867f-6a18a117fee8");
@Autowired
private GeonamesISOLanguageSource isoLanguageSource;
/** The Constant LOG. */
public static final Log LOG = LogFactory.getLog(ISO639VocabularyUpdater.class);
/**
* Generates a current ISO639-3 {@link ControlledVocabulary} but doesn't
* persist it to storage.
*
* @return vocabulary of ISO639-3 3-letter language codes
* @throws IOException IOException
*/
public ControlledVocabulary getISO639Vocabulary() throws IOException {
return createVocabulary("ISO639-3", LanguageInfo::getCode);
}
@Autowired
private GeonamesISOLanguageSource isoLanguageSource;
/**
* Creates the vocabulary.
*
* @param title the title
* @param toTerm the to term
* @return the controlled vocabulary
* @throws IOException Signals that an I/O exception has occurred.
*/
protected ControlledVocabulary createVocabulary(final String title, final Function<LanguageInfo, String> toTerm) throws IOException {
final ControlledVocabulary vocabulary = new ControlledVocabulary();
vocabulary.setTitle(title);
/**
* Generates a current ISO639-3 {@link ControlledVocabulary} but doesn't persist
* it to storage.
*
* @return vocabulary of ISO639-3 3-letter language codes
* @throws IOException IOException
*/
public ControlledVocabulary getISO639Vocabulary() throws IOException {
return createVocabulary("ISO639-3", LanguageInfo::getCode);
}
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy.MM.dd");
final LocalDate localDate = LocalDate.now();
vocabulary.setVersionTag(dtf.format(localDate));
/**
* Creates the vocabulary.
*
* @param title the title
* @param toTerm the to term
* @return the controlled vocabulary
* @throws IOException Signals that an I/O exception has occurred.
*/
protected ControlledVocabulary createVocabulary(final String title, final Function<LanguageInfo, String> toTerm) throws IOException {
final ControlledVocabulary vocabulary = new ControlledVocabulary();
vocabulary.setTitle(title);
vocabulary.setUrl(GeonamesISOLanguageSource.GEONAMES_ISO639_URL);
final Map<String, VocabularyTerm> assignedCodes = new HashMap<>();
isoLanguageSource.fetchLanguageData().stream().map(language -> {
final VocabularyTerm term = new VocabularyTerm();
term.setCode(toTerm.apply(language));
term.setTitle(language.getLanguage());
return term;
})
// remove terms without codes
.filter(term -> (term.getCode() != null) && (term.getCode().length() > 0))
// add to vocabularyTerms if ISO language code is not assigned
.forEach(term -> {
if (!assignedCodes.containsKey(term.getCode())) {
assignedCodes.put(term.getCode(), term);
}
});
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy.MM.dd");
final LocalDate localDate = LocalDate.now();
vocabulary.setVersionTag(dtf.format(localDate));
vocabulary.setTerms(new ArrayList<>(assignedCodes.values()));
return vocabulary;
}
final Map<String, VocabularyTerm> assignedCodes = new HashMap<>();
isoLanguageSource.fetchLanguageData().stream().map(language -> {
final VocabularyTerm term = new VocabularyTerm();
term.setCode(toTerm.apply(language));
term.setTitle(language.getLanguage());
return term;
})
// remove terms without codes
.filter(term -> (term.getCode() != null) && (term.getCode().length() > 0))
// add to vocabularyTerms if ISO language code is not assigned
.forEach(term -> {
if (!assignedCodes.containsKey(term.getCode())) {
assignedCodes.put(term.getCode(), term);
}
});
vocabulary.setTerms(new ArrayList<>(assignedCodes.values()));
return vocabulary;
}
}
......@@ -36,6 +36,9 @@ db.updateSchema=true
#db.pool.initialSize=5
#db.pool.maxActive=10
# Default partner UUID
partner.primary.uuid=a870b3b2-1dfc-459f-abc8-8c10ca8aaab4
file.repository.path=/file_repository
#AWS parameter values
......
......@@ -30,7 +30,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true /*, securedEnabled = true */)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
......@@ -38,12 +38,21 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
// @Bean
// public AuthenticationProvider runAsAuthenticationProvider() {
// RunAsImplAuthenticationProvider authProvider = new RunAsImplAuthenticationProvider();
// authProvider.setKey("MyRunAsKey");
// return authProvider;
// }
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
auth
//.authenticationProvider(runAsAuthenticationProvider())
.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
......
......@@ -21,6 +21,8 @@ import java.util.concurrent.TimeUnit;
import org.genesys.catalog.server.components.AddStuffInterceptor;
import org.genesys.catalog.server.components.StartupInitializer;
import org.genesys.catalog.service.worker.GeonamesISOLanguageSource;
import org.genesys.catalog.service.worker.ISO639VocabularyUpdater;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
......@@ -115,6 +117,16 @@ public class WebConfiguration extends WebMvcConfigurerAdapter {
return cookieLocaleResolver;
}
@Bean
public ISO639VocabularyUpdater iSO639VocabularyUpdater() {
return new ISO639VocabularyUpdater();
}
@Bean
public GeonamesISOLanguageSource geonamesISOLanguageSource() {
return new GeonamesISOLanguageSource();
}
@Bean
public AddStuffInterceptor addStuffInterceptor() {
return new AddStuffInterceptor();
......
/*
* Copyright 2018 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.genesys.catalog.server.controller.api.v0;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.genesys.catalog.exceptions.NotFoundElement;
import org.genesys.catalog.model.vocab.ControlledVocabulary;
import org.genesys.catalog.model.vocab.VocabularyTerm;
import org.genesys.catalog.service.PartnerService;
import org.genesys.catalog.service.VocabularyService;
import org.genesys.catalog.service.worker.ISO639VocabularyUpdater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* The Class LanguagesController.
*
* @author Maxym Borodenko
*/
@RestController
@RequestMapping(LanguagesController.API_BASE)
@PreAuthorize("isAuthenticated()")
public class LanguagesController implements InitializingBean {
protected static final String API_BASE = "/api/v0/lang";
public static final UUID ISO639_3 = ISO639VocabularyUpdater.ISO639_3;
private static final Logger LOG = LoggerFactory.getLogger(LanguagesController.class);
@Autowired
private ISO639VocabularyUpdater iso639VocabularyUpdater;
@Autowired
private VocabularyService vocabularyService;
@Autowired
private PartnerService partnerService;
@Override
public void afterPropertiesSet() throws Exception {
final ControlledVocabulary iso639 = vocabularyService.getVocabulary(ISO639_3);
if (iso639 == null) {
ControlledVocabulary updated = iso639VocabularyUpdater.getISO639Vocabulary();
updated.setUuid(ISO639_3);
updated.setOwner(partnerService.getPrimaryPartner());
updated = vocabularyService.createVocabulary(updated);
LOG.info("Created ISO639-3 vocabulary with {} terms", updated.getTerms().size());
}
}
@PreAuthorize("hasRole('ADMINISTRATOR')")
@PostMapping(value = "/update")
public ControlledVocabulary updateLanguages() throws IOException {
final ControlledVocabulary iso639 = vocabularyService.getVocabulary(ISO639_3);
ControlledVocabulary updated = iso639VocabularyUpdater.getISO639Vocabulary();
updated.setUuid(iso639.getUuid());
updated.setVersion(iso639.getVersion());
LOG.info("Updating ISO639-3 vocabulary with {} terms", updated.getTerms().size());
return vocabularyService.updateVocabulary(updated);
}
@GetMapping(value = "/{code}", produces = MediaType.APPLICATION_JSON_VALUE)
public VocabularyTerm get(@PathVariable("code") final String code) {
ControlledVocabulary iso639 = vocabularyService.getVocabulary(ISO639_3);
return iso639.getTerms().stream().filter(term -> term.getCode().toLowerCase().equals(code.toLowerCase())).findFirst().orElseThrow(() -> new NotFoundElement(
"Language with such code not found"));
}
@GetMapping(value = "/autocomplete", produces = MediaType.APPLICATION_JSON_VALUE)
public List<VocabularyTerm> autocomplete(@RequestParam("l") final String languageLike) throws IOException {
if (languageLike.length() < 3)
return Collections.emptyList();
ControlledVocabulary iso639 = vocabularyService.getVocabulary(ISO639_3);
return iso639.getTerms().stream().filter(term -> term.getTitle().toLowerCase().startsWith(languageLike.toLowerCase())).limit(20).collect(Collectors.toList());
}
}
......@@ -23,10 +23,12 @@ db.pool.maxActive=10
# WARNING: Make sure liquibase-changeLog.yml contains required changes. Do not rely on db.updateSchema!
db.updateSchema=false
default.admin.email=admin@example.com
default.admin.password=Admin123!
# Default partner UUID
partner.primary.uuid=39d3022b-dfca-45d8-98f1-3eeaa6c3e605
default.oauthclient.clientId=my-trusted-client
default.oauthclient.clientSecret=my-secret-client
......
/*
* Copyright 2018 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.genesys.catalog.server.controller.api.v0;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.genesys.catalog.server.controller.rest.AbstractRestTest;
import org.genesys.catalog.service.worker.GeonamesISOLanguageSource.LanguageInfo;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
/**
* The Class LanguagesControllerTest.
*
* @author Maxym Borodenko
*/
public class LanguagesControllerTest extends AbstractRestTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
@Transactional
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).apply(documentationConfiguration(restDocumentation).uris().withScheme("https").withHost(
"api.catalog.genesys-pgr.org").withPort(443)).build();
}
@Test
@WithMockUser(username = "user", password = "user", roles = "ADMINISTRATOR")
public void getLanguageInfoByCodeTest() throws Exception {
final LanguageInfo languageInfo = new LanguageInfo("ukr", "Ukrainian");
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders.get(LanguagesController.API_BASE.concat("/{code}"), languageInfo.getCode()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$.code", is(languageInfo.getCode())))
.andExpect(jsonPath("$.title", is(languageInfo.getLanguage())));
/*@formatter:on*/
}
@Test
@WithMockUser(username = "user", password = "user", roles = "ADMINISTRATOR")
public void listLanguageInfoTest() throws Exception {
final LanguageInfo languageInfo = new LanguageInfo("ukr", "Ukrainian");
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders.get(LanguagesController.API_BASE.concat("/autocomplete"))
.param("l", languageInfo.getLanguage().substring(0, 4))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("$.[0]", not(nullValue())))
.andExpect(jsonPath("$.[0].code", not(nullValue())))
.andExpect(jsonPath("$.[0].title", not(nullValue())));
/*@formatter:on*/
}
@Test
@WithMockUser(username = "user", password = "user", roles = "ADMINISTRATOR")
public void searchTermToShortTest() throws Exception {
final LanguageInfo languageInfo = new LanguageInfo("ukr", "Ukrainian");
/*@formatter:off*/
mockMvc
.perform(RestDocumentationRequestBuilders.get(LanguagesController.API_BASE.concat("/autocomplete"))
.param("l", languageInfo.getLanguage().substring(0, 2))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(content().string("[]"));
/*@formatter:on*/
}
}
......@@ -33,6 +33,8 @@ import org.genesys.catalog.server.controller.api.v0.LocationController;
import org.genesys.catalog.server.controller.api.v0.PartnerController;
import org.genesys.catalog.server.controller.api.v0.PermissionController;
import org.genesys.catalog.server.tests.ServiceTest;
import org.genesys.catalog.service.worker.GeonamesISOLanguageSource;
import org.genesys.catalog.service.worker.ISO639VocabularyUpdater;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
......@@ -89,6 +91,16 @@ public abstract class AbstractRestTest extends ServiceTest {
return new OAuthServiceImpl();
}
@Bean
public ISO639VocabularyUpdater iSO639VocabularyUpdater() {
return new ISO639VocabularyUpdater();
}
@Bean
public GeonamesISOLanguageSource geonamesISOLanguageSource() {
return new GeonamesISOLanguageSource();
}