package ca.intelliware.ihtsdo.mlds.web.rest;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Resource;
import javax.annotation.security.RolesAllowed;
import javax.transaction.Transactional;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import ca.intelliware.ihtsdo.mlds.domain.Affiliate;
import ca.intelliware.ihtsdo.mlds.domain.AffiliateDetails;
import ca.intelliware.ihtsdo.mlds.domain.Application;
import ca.intelliware.ihtsdo.mlds.domain.ApprovalState;
import ca.intelliware.ihtsdo.mlds.domain.MailingAddress;
import ca.intelliware.ihtsdo.mlds.domain.Member;
import ca.intelliware.ihtsdo.mlds.domain.StandingState;
import ca.intelliware.ihtsdo.mlds.domain.User;
import ca.intelliware.ihtsdo.mlds.repository.AffiliateDetailsRepository;
import ca.intelliware.ihtsdo.mlds.repository.AffiliateRepository;
import ca.intelliware.ihtsdo.mlds.repository.AffiliateSearchRepository;
import ca.intelliware.ihtsdo.mlds.repository.MemberRepository;
import ca.intelliware.ihtsdo.mlds.repository.UserRepository;
import ca.intelliware.ihtsdo.mlds.security.AuthoritiesConstants;
import ca.intelliware.ihtsdo.mlds.security.ihtsdo.CurrentSecurityContext;
import ca.intelliware.ihtsdo.mlds.service.AffiliateAuditEvents;
import ca.intelliware.ihtsdo.mlds.service.AffiliateDeleter;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.AffiliateImportAuditEvents;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.AffiliatesExporterService;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.AffiliatesImportGenerator;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.AffiliatesImportSpec;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.AffiliatesImporterService;
import ca.intelliware.ihtsdo.mlds.service.affiliatesimport.ImportResult;
import ca.intelliware.ihtsdo.mlds.web.SessionService;
@RestController
public class AffiliateResource {
private final Logger log = LoggerFactory.getLogger(AffiliateResource.class);
@Resource
AffiliateRepository affiliateRepository;
@Resource
AffiliateSearchRepository affiliateSearchRepository;
@Resource
AffiliateDetailsRepository affiliateDetailsRepository;
@Resource
ApplicationAuthorizationChecker applicationAuthorizationChecker;
@Resource
AffiliateAuditEvents affiliateAuditEvents;
@Resource
AffiliateImportAuditEvents affiliateImportAuditEvents;
@Resource
AffiliatesImporterService affiliatesImporterService;
@Resource
AffiliatesExporterService affiliatesExporterService;
@Resource
AffiliatesImportGenerator affiliatesImportGenerator;
@Resource
UserRepository userRepository;
@Resource
MemberRepository memberRepository;
@Resource
AffiliateDeleter affiliateDeleter;
@Resource
SessionService sessionService;
@Resource
CurrentSecurityContext currentSecurityContext;
public static final int DEFAULT_PAGE_SIZE = 50;
public static final String FILTER_HOME_MEMBER = "homeMember eq '(\\w+)'";
public static final String FILTER_STANDING = "(not)?\\s?standingState eq '(\\w+)'";
@RolesAllowed({ AuthoritiesConstants.STAFF, AuthoritiesConstants.ADMIN })
@RequestMapping(value = Routes.AFFILIATES,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<Collection<Affiliate>> getAffiliates(
@RequestParam(required=false) String q,
@RequestParam(value="$page", defaultValue="0", required=false) Integer page,
@RequestParam(value="$pageSize", defaultValue="50", required=false) Integer pageSize,
@RequestParam(value="$filter", required=false) List<String> filters,
@RequestParam(value="$orderby", required=false) String orderby) {
Page<Affiliate> affiliates;
Sort sort = createAffiliatesSort(orderby);
PageRequest pageRequest = new PageRequest(page, pageSize, sort);
Member member = null;
StandingState standingState = null;
boolean standingStateNot = false;
if (filters != null && filters.size() > 0 && StringUtils.isNotBlank(filters.get(0))) {
for (String filter : filters) {
Matcher homeMemberMatcher = Pattern.compile(FILTER_HOME_MEMBER).matcher(filter);
if (homeMemberMatcher.matches()) {
String homeMember = homeMemberMatcher.group(1);
member = memberRepository.findOneByKey(homeMember);
continue;
}
Matcher standingStateMatcher = Pattern.compile(FILTER_STANDING).matcher(filter);
if (standingStateMatcher.matches()) {
standingStateNot = StringUtils.equalsIgnoreCase("not", standingStateMatcher.group(1));
String standingStateString = standingStateMatcher.group(2);
standingState = StandingState.valueOf(standingStateString);
continue;
}
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
if (!StringUtils.isBlank(q)) {
//Note that sorting in the pageRequest is not currently respected by lucene
affiliates = affiliateSearchRepository.findFullTextAndMember(q, member, standingState, standingStateNot, pageRequest) ;
} else {
if (member == null) {
if (standingState == null) {
affiliates = affiliateRepository.findAll(pageRequest);
} else {
if (standingStateNot) {
affiliates = affiliateRepository.findByStandingStateNot(standingState, pageRequest);
} else {
affiliates = affiliateRepository.findByStandingState(standingState, pageRequest);
}
}
} else {
if (standingState == null) {
affiliates = affiliateRepository.findByHomeMember(member, pageRequest);
} else {
if (standingStateNot) {
affiliates = affiliateRepository.findByHomeMemberAndStandingStateNot(member, standingState, pageRequest);
} else {
affiliates = affiliateRepository.findByHomeMemberAndStandingState(member, standingState, pageRequest);
}
}
}
}
return new ResponseEntity<Collection<Affiliate>>(affiliates.getContent(), HttpStatus.OK);
}
private static final Map<String,List<String>> ORDER_BY_FIELD_MAPPINGS = new HashMap<String,List<String>>();
static {
//FIXME using both the affiliateDetails and the application.affiliateDetails causes discrepancies in the order and text shown on the front end. Perhaps we should keep affiliateDetails up to date with primary application affiliateDetail updates?
ORDER_BY_FIELD_MAPPINGS.put("affiliateId", Arrays.asList("affiliateId"));
ORDER_BY_FIELD_MAPPINGS.put("name", Arrays.asList("affiliateDetails.firstName", "affiliateDetails.lastName"));
ORDER_BY_FIELD_MAPPINGS.put("agreementType", Arrays.asList("affiliateDetails.type", "affiliateDetails.subType"));
ORDER_BY_FIELD_MAPPINGS.put("standingState", Arrays.asList("standingState"));
ORDER_BY_FIELD_MAPPINGS.put("homeCountry", Arrays.asList("affiliateDetails.address.country.commonName"));
ORDER_BY_FIELD_MAPPINGS.put("member", Arrays.asList("homeMember.key"));
ORDER_BY_FIELD_MAPPINGS.put("email", Arrays.asList("affiliateDetails.email"));
}
private Sort createAffiliatesSort(String orderby) {
Sort defaultSort = new Sort(
// new Order(Direction.ASC, "affiliateDetails.organizationName"),
// new Order(Direction.ASC, "affiliateDetails.firstName"),
// new Order(Direction.ASC, "affiliateDetails.lastName"),
// new Order(Direction.ASC, "application.affiliateDetails.organizationName"),
// new Order(Direction.ASC, "application.affiliateDetails.firstName"),
// new Order(Direction.ASC, "application.affiliateDetails.lastName"),
new Order(Direction.ASC, "affiliateId")
);
return new SortBuilder().createSort(orderby, ORDER_BY_FIELD_MAPPINGS, defaultSort);
}
@RolesAllowed({ AuthoritiesConstants.STAFF, AuthoritiesConstants.ADMIN })
@RequestMapping(value = Routes.AFFILIATE,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<Affiliate> getAffiliate(@PathVariable long affiliateId) {
Affiliate affiliate = affiliateRepository.findOne(affiliateId);
if (affiliate == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Affiliate>(affiliate, HttpStatus.OK);
}
@RolesAllowed({ AuthoritiesConstants.STAFF, AuthoritiesConstants.ADMIN })
@RequestMapping(value = Routes.AFFILIATE,
method = RequestMethod.PUT,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<Affiliate> updateAffiliate(@PathVariable Long affiliateId, @RequestBody Affiliate body) {
Affiliate affiliate = affiliateRepository.findOne(affiliateId);
if (affiliate == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
applicationAuthorizationChecker.checkCanManageAffiliate(affiliate);
StandingState originalStanding = affiliate.getStandingState();
copyAffiliateFields(affiliate, body);
if (! Objects.equal(originalStanding, affiliate.getStandingState())) {
if (Objects.equal(originalStanding, StandingState.APPLYING)
|| Objects.equal(originalStanding, StandingState.REJECTED)) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
} else {
affiliateAuditEvents.logStandingStateChange(affiliate);
}
}
affiliateRepository.save(affiliate);
affiliateAuditEvents.logUpdateOfAffiliate(affiliate);
return new ResponseEntity<Affiliate>(affiliate, HttpStatus.OK);
}
private void copyAffiliateFields(Affiliate affiliate, Affiliate body) {
affiliate.setNotesInternal(body.getNotesInternal());
if (body.getStandingState() != null) {
affiliate.setStandingState(body.getStandingState());
}
}
@RolesAllowed({ AuthoritiesConstants.STAFF, AuthoritiesConstants.ADMIN })
@RequestMapping(value = Routes.AFFILIATE,
method = RequestMethod.DELETE,
produces = MediaType.APPLICATION_JSON_VALUE)
@Transactional
@Timed
public @ResponseBody ResponseEntity<Affiliate> deleteAffiliate(@PathVariable Long affiliateId) {
Affiliate affiliate = affiliateRepository.findOne(affiliateId);
if (affiliate == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
applicationAuthorizationChecker.checkCanManageAffiliate(affiliate);
if (!ObjectUtils.equals(affiliate.getStandingState(), StandingState.APPLYING)) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
affiliateAuditEvents.logDeleteOfAffiliate(affiliate);
affiliateDeleter.deleteAffiliate(affiliate);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@RolesAllowed({AuthoritiesConstants.USER})
@RequestMapping(value = Routes.AFFILIATES_ME,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<Collection<Affiliate>> getAffiliatesMe() {
String username = sessionService.getUsernameOrNull();
return new ResponseEntity<Collection<Affiliate>>(affiliateRepository.findByCreatorIgnoreCase(username), HttpStatus.OK);
}
@RolesAllowed({AuthoritiesConstants.USER})
@RequestMapping(value = Routes.AFFILIATES_CREATOR,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<Collection<Affiliate>> getAffiliatesForUser(@PathVariable String username) {
applicationAuthorizationChecker.checkCanAccessAffiliate(username);
return new ResponseEntity<Collection<Affiliate>>(affiliateRepository.findByCreatorIgnoreCase(username), HttpStatus.OK);
}
@RolesAllowed({AuthoritiesConstants.ADMIN})
@RequestMapping(value = Routes.AFFILIATES_CSV,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<ImportResult> importAffiliates( @RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
//FIXME Is this correct that we are assuming UTF8?
String content = IOUtils.toString(file.getInputStream(), Charsets.UTF_8);
ImportResult importResult = affiliatesImporterService.importFromCSV(content);
affiliateImportAuditEvents.logImport(importResult);
HttpStatus httpStatus = importResult.isSuccess() ? HttpStatus.OK : HttpStatus.BAD_REQUEST;
return new ResponseEntity<ImportResult>(importResult, httpStatus);
}
@RolesAllowed({AuthoritiesConstants.ADMIN})
@RequestMapping(value = Routes.AFFILIATES_CSV,
method = RequestMethod.GET,
produces = "application/csv;charset=UTF-8")
@Timed
public @ResponseBody ResponseEntity<String> exportAffiliates(@RequestParam(value="generate",required = false) Integer generateRows) throws IOException {
//FIXME DGJ Introduce parameter to generate phoney data until we can add an application start
String result;
if (generateRows == null) {
result = affiliatesExporterService.exportToCSV();
} else {
result = affiliatesImportGenerator.generateFile(generateRows);
}
affiliateImportAuditEvents.logExport();
return new ResponseEntity<String>(result, HttpStatus.OK);
}
//FIXME Would like to use custom produces to overload request path
@RolesAllowed({AuthoritiesConstants.ADMIN})
@RequestMapping(value = Routes.AFFILIATES_CSV_SPEC,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<AffiliatesImportSpec> getAffiliatesImportSpec() throws IOException {
AffiliatesImportSpec result = affiliatesExporterService.exportSpec();
return new ResponseEntity<AffiliatesImportSpec>(result, HttpStatus.OK);
}
@RolesAllowed({AuthoritiesConstants.USER})
@RequestMapping(value = Routes.AFFILIATE_DETAIL,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
public @ResponseBody ResponseEntity<AffiliateDetails> updateAffiliateDetail(@PathVariable Long affiliateId) {
Affiliate affiliate = affiliateRepository.findOne(affiliateId);
applicationAuthorizationChecker.checkCanAccessAffiliate(affiliate);
if (affiliate == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<AffiliateDetails>(affiliate.getAffiliateDetails(), HttpStatus.OK);
}
@SuppressWarnings("unchecked")
@RolesAllowed({AuthoritiesConstants.USER, AuthoritiesConstants.STAFF, AuthoritiesConstants.ADMIN})
@RequestMapping(value = Routes.AFFILIATE_DETAIL,
method = RequestMethod.PUT,
produces = MediaType.APPLICATION_JSON_VALUE)
@Timed
@Transactional
public @ResponseBody ResponseEntity<?> updateAffiliateDetail(@PathVariable Long affiliateId, @RequestBody AffiliateDetails body) {
Affiliate affiliate = affiliateRepository.findOne(affiliateId);
applicationAuthorizationChecker.checkCanAccessAffiliate(affiliate);
if (affiliate == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
AffiliateDetails affiliateDetails = affiliate.getAffiliateDetails();
if (affiliateDetails == null) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
Application application = affiliate.getApplication();
if (application == null || !Objects.equal(application.getApprovalState(), ApprovalState.APPROVED)) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
String originalEmail = affiliateDetails.getEmail();
String newEmail = body.getEmail();
boolean emailChanged = !Objects.equal(newEmail, originalEmail);
if (emailChanged) {
if (!currentSecurityContext.isStaffOrAdmin()) {
return new ResponseEntity<>("Users may not change their primary email address",HttpStatus.FORBIDDEN);
}
if (Strings.isNullOrEmpty(newEmail)) {
return new ResponseEntity<>("Primary email address (email) is a required field",HttpStatus.BAD_REQUEST);
}
affiliate.setCreator(newEmail);
}
User user = userRepository.findByLoginIgnoreCase(originalEmail);
if (user != null) {
copyAffiliateDetailsNameFieldsToUser(user, body);
}
affiliateAuditEvents.logUpdateOfAffiliateDetails(affiliate,body);
copyAffiliateDetailsFields(affiliateDetails, body);
affiliateSearchRepository.reindex(affiliate);
return new ResponseEntity<AffiliateDetails>(affiliateDetails, HttpStatus.OK);
}
private void copyAffiliateDetailsFields(AffiliateDetails affiliateDetails, AffiliateDetails body) {
copyAddressFieldsWithoutCountry(affiliateDetails.getAddress(), body.getAddress());
copyAddressFields(affiliateDetails.getBillingAddress(), body.getBillingAddress());
affiliateDetails.setFirstName(body.getFirstName());
affiliateDetails.setLandlineExtension(body.getLandlineExtension());
affiliateDetails.setLandlineNumber(body.getLandlineNumber());
affiliateDetails.setLastName(body.getLastName());
affiliateDetails.setMobileNumber(body.getMobileNumber());
affiliateDetails.setAlternateEmail(body.getAlternateEmail());
affiliateDetails.setThirdEmail(body.getThirdEmail());
affiliateDetails.setEmail(body.getEmail());
if (currentSecurityContext.isStaffOrAdmin()) {
affiliateDetails.setType(body.getType());
affiliateDetails.setOtherText(body.getOtherText());
affiliateDetails.setSubType(body.getSubType());
affiliateDetails.setAgreementType(body.getAgreementType());
}
}
private void copyAddressFields(MailingAddress address, MailingAddress body) {
copyAddressFieldsWithoutCountry(address, body);
address.setCountry(body.getCountry());
}
private void copyAddressFieldsWithoutCountry(MailingAddress address, MailingAddress body) {
address.setCity(body.getCity());
address.setPost(body.getPost());
address.setStreet(body.getStreet());
}
private void copyAffiliateDetailsNameFieldsToUser(User user, AffiliateDetails body) {
user.setFirstName(body.getFirstName());
user.setLastName(body.getLastName());
user.setEmail(body.getEmail());
user.setLogin(body.getEmail());
}
}