package org.zalando.catwatch.backend.web; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Sets.SetView; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections.comparators.ComparatorChain; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Controller; 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.zalando.catwatch.backend.model.Contributor; import org.zalando.catwatch.backend.model.ContributorKey; import org.zalando.catwatch.backend.repo.ContributorRepository; import org.zalando.catwatch.backend.util.Constants; import javax.persistence.EmbeddedId; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Map; import static com.google.common.base.Joiner.on; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Sets.intersection; import static java.time.Instant.now; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.Collections.unmodifiableList; import static java.util.Date.from; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.zalando.catwatch.backend.util.Constants.CONFIG_ORGANIZATION_LIST; import static org.zalando.catwatch.backend.web.config.DateUtil.iso8601; @Controller @RequestMapping(value = Constants.API_RESOURCE_CONTRIBUTORS, produces = {APPLICATION_JSON_VALUE}) @Api(value = Constants.API_RESOURCE_CONTRIBUTORS, description = "the contributors API") public class ContributorsApi { private final static int LIMIT_DEFAULT = 5; private final static List<String> SORT_BY_LIST = unmodifiableList( asList("organizationalCommitsCount", "organizationalProjectsCount", "personalCommitsCount", "personalProjectsCount", "organizationName", "name")); @EmbeddedId private ContributorKey key; private final ContributorRepository repository; private final Environment env; @Autowired public ContributorsApi(ContributorRepository repository, Environment env) { this.repository = repository; this.env = env; } @ApiOperation(value = "Contributor", notes = "The Contributors endpoint returns all information like name, url, commits count, \nprojects count of all the Contributors for the selected filter. \n", response = Contributor.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 200, message = "An array of Contributors of selected Github organization"), @ApiResponse(code = 0, message = "Unexpected error")}) @RequestMapping(value = "", method = RequestMethod.GET) public @ResponseBody List<Contributor> contributorsGet( @ApiParam(value = "List of github.com organizations to scan(comma seperated)", required = true) // @RequestParam(value = Constants.API_REQUEST_PARAM_ORGANIZATIONS, required = true) // String organizations, // @ApiParam(value = "Number of items to retrieve. Default is 5.") // @RequestParam(value = Constants.API_REQUEST_PARAM_LIMIT, required = false) // Integer limit, // @ApiParam(value = "Offset the list of returned results by this amount. Default is zero.") // @RequestParam(value = Constants.API_REQUEST_PARAM_OFFSET, required = false) // Integer offset, // @ApiParam(value = "Date from which to start fetching records from database(default = current_date)") // @RequestParam(value = Constants.API_REQUEST_PARAM_STARTDATE, required = false) // String startDate, // @ApiParam(value = "Date till which records will be fetched from database(default = current_date)") // @RequestParam(value = Constants.API_REQUEST_PARAM_ENDDATE, required = false) // String endDate, // @ApiParam(value = "parameter by which result should be sorted. '-' means descending order (default is count of commit)") // @RequestParam(value = Constants.API_REQUEST_PARAM_SORTBY, required = false) // String sortBy, // @ApiParam(value = "query paramater for search query (this will be contributor names prefix)") // @RequestParam(value = Constants.API_REQUEST_PARAM_Q, required = false) // String q // ) { validate(organizations, offset, limit, sortBy, startDate, endDate); if (startDate != null && endDate != null && repository.findPreviousSnapShotDate(iso8601(endDate)) != null && repository.findPreviousSnapShotDate(iso8601(startDate)) != null) { return contributorsGet_timeSpan(organizations, limit, offset, startDate, endDate, sortBy, q); } else if (startDate == null && endDate == null // && repository.findPreviousSnapShotDate(from(now())) != null) { return contributorsGet_noTimeSpan(organizations, limit, offset, endDate, sortBy, q); } else { throw new UnsupportedOperationException( "this parameter configuration is not implemented yet" + " .. start date, end date required atm"); } } private List<Contributor> contributorsGet_noTimeSpan(String organizations, Integer limit, Integer offset, String endDate, String sortBy, String q) { Date endDateDate = endDate != null ? iso8601(endDate) : new Date(); Date endDateInDb = repository.findPreviousSnapShotDate(endDateDate); ArrayListMultimap<Long, Contributor> multiMap = ArrayListMultimap.create(); orgs(organizations).values().stream().forEach(organizationId -> { List<Contributor> contributors = repository.findAllTimeTopContributors(organizationId, endDateInDb, q, null, null); contributors.stream().forEach(c -> multiMap.put(c.getKey().getId(), c)); }); List<Contributor> sorted = multiMap.asMap().values().stream().map(list -> add(list)).collect(toList()); Collections.sort(sorted, sortBy(sortBy)); return sublist(limit, offset, sorted); } private List<Contributor> contributorsGet_timeSpan(String organizations, Integer limit, Integer offset, String startDate, String endDate, String sortBy, String q) { Date startDateInDb = repository.findPreviousSnapShotDate(iso8601(startDate)); Date endDateInDb = repository.findPreviousSnapShotDate(iso8601(endDate)); checkNotNull(startDateInDb); checkNotNull(endDateInDb); ArrayListMultimap<Long, Contributor> multiMap = ArrayListMultimap.create(); orgs(organizations).values().stream().forEach(organizationId -> { List<Contributor> startData = repository.findAllTimeTopContributors(organizationId, startDateInDb, q, null, null); Map<Long, Contributor> startMap = startData.stream().collect(toMap(Contributor::getId, identity())); List<Contributor> endData = repository.findAllTimeTopContributors(organizationId, endDateInDb, q, null, null); Map<Long, Contributor> endMap = endData.stream().collect(toMap(Contributor::getId, identity())); SetView<Long> contributorIds = intersection(startMap.keySet(), endMap.keySet()); contributorIds.stream() // .map(id -> diff(startMap.get(id), endMap.get(id))) // .forEach(c -> multiMap.put(c.getKey().getId(), c)); }); List<Contributor> sorted = multiMap.asMap().values().stream().map(list -> add(list)).collect(toList()); Collections.sort(sorted, sortBy(sortBy)); return sublist(limit, offset, sorted); } // // util // private List<Contributor> sublist(Integer limit, Integer offset, List<Contributor> sorted) { int endIndex; if (offset(offset) + limit(limit) > sorted.size()) { endIndex = sorted.size(); } else { endIndex = offset(offset) + limit(limit); } sorted = sorted.subList(offset(offset), endIndex); return sorted; } // // validate / process arguments // private void validate(String organizations, Integer offset, Integer limit, String sortBy, String startDate, String endDate) { checkArgument(offset(offset) >= 0, "offset must be greater than zero but was " + offset); checkArgument(limit(limit) > 0, "limit must be greater than zero but was " + limit); checkArgument(!orgs(organizations).containsValue(null), "an organization name was not found: " + organizations); checkArgument(sortBy(sortBy) != null, "sortBy must be empty or have a valid value but was " + sortBy + ". Valid values are " + on(",").join(SORT_BY_LIST)); checkArgument(endDate == null || repository.findPreviousSnapShotDate(iso8601(endDate)) != null, "endDate is set to " + endDate + "but there is no snapshot data before that date"); checkArgument(startDate == null || repository.findPreviousSnapShotDate(iso8601(startDate)) != null, "startDate is set to " + startDate + "but there is no snapshot data before that date"); checkArgument(startDate == null || endDate == null || iso8601(startDate).before(iso8601(endDate)), "startDate " + startDate + " must be before endDate" + endDate + " but was not"); } private Map<String, Long> orgs(String organizations) { if (isNullOrEmpty(organizations)) { organizations = env.getProperty(CONFIG_ORGANIZATION_LIST); } return stream(organizations.trim().split("\\s*,\\s*")) .collect(toMap(identity(), orgName -> repository.findOrganizationId(orgName))); } private int offset(Integer offset) { return offset == null ? 0 : offset; } private int limit(Integer limit) { if (limit != null) { return limit; } else { return LIMIT_DEFAULT; } } private Comparator<Contributor> sortBy(String sortBy) { if (Strings.isNullOrEmpty(sortBy)) { return comparator(SORT_BY_LIST.get(0), true); } else { // (this should be re-written or/and unit tested) sortBy = sortBy.trim(); boolean reverse = false; if (sortBy.startsWith("-")) { reverse = true; sortBy = sortBy.substring(1); } String cleanedSortBy = SORT_BY_LIST.stream().collect(toMap(String::toLowerCase, identity())) .get(sortBy.toLowerCase()); if (cleanedSortBy != null) { return comparator(cleanedSortBy, reverse); } else { return null; } } } // // process data // private Contributor add(Collection<Contributor> collection) { Contributor any = collection.iterator().next(); Contributor c = new Contributor(any.getId(), any.getOrganizationId(), any.getSnapshotDate()); c.setName(any.getName()); c.setUrl(any.getUrl()); c.setOrganizationalCommitsCount(0); c.setOrganizationalProjectsCount(0); c.setPersonalCommitsCount(0); c.setPersonalProjectsCount(0); for (Contributor cc : collection) { c.setOrganizationalCommitsCount(add(c.getOrganizationalCommitsCount(), cc.getOrganizationalCommitsCount())); c.setOrganizationalProjectsCount( add(c.getOrganizationalProjectsCount(), cc.getOrganizationalProjectsCount())); c.setPersonalCommitsCount(add(c.getPersonalCommitsCount(), cc.getPersonalCommitsCount())); c.setPersonalProjectsCount(add(c.getPersonalProjectsCount(), cc.getPersonalProjectsCount())); } return c; } private Contributor diff(Contributor start, Contributor end) { Contributor c = new Contributor(end.getId(), end.getOrganizationId(), end.getSnapshotDate()); c.setName(end.getName()); c.setUrl(end.getUrl()); c.setOrganizationalCommitsCount( subtract(end.getOrganizationalCommitsCount(), start.getOrganizationalCommitsCount())); c.setOrganizationalProjectsCount( subtract(end.getOrganizationalProjectsCount(), start.getOrganizationalProjectsCount())); c.setPersonalCommitsCount(subtract(end.getPersonalCommitsCount(), start.getPersonalCommitsCount())); c.setPersonalProjectsCount(subtract(end.getPersonalProjectsCount(), start.getPersonalProjectsCount())); return c; } private Integer subtract(Integer x, Integer y) { return (x != null && y != null) ? x - y : null; } private Integer add(Integer x, Integer y) { return (x != null && y != null) ? x + y : null; } @SuppressWarnings("unchecked") private Comparator<Contributor> comparator(String sortBy, boolean reverse) { ComparatorChain comparator = new ComparatorChain(); comparator.addComparator(new BeanComparator<Contributor>(sortBy), reverse); comparator.addComparator(new BeanComparator<Contributor>("id")); return comparator; } }