CallsController.java

/*
 * Copyright 2020 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.gringlobal.brapi.v1;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.gringlobal.brapi.BaseBrAPIController;
import org.gringlobal.brapi.BrAPIPage;
import org.gringlobal.brapi.BrAPIResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * The BrAPI CallsController.
 */
@RestController("brapiCalls")
@RequestMapping(CropsController.BRAPIv1_BASE)
@Slf4j
public class CallsController extends BaseBrAPIController implements InitializingBean {

	private static final RequestMethod[] GET_MAPPING = { RequestMethod.GET };
	private static final RequestMethod[] POST_MAPPING = { RequestMethod.POST };
	private static final RequestMethod[] PUT_MAPPING = { RequestMethod.PUT };
	private static final RequestMethod[] DELETE_MAPPING = { RequestMethod.DELETE };
	private List<Call> supportedCalls;

	@Override
	public void afterPropertiesSet() throws Exception {
		// Scan the org.gringlobal.brapi.v1 package for @Controller annotations
		supportedCalls = new ArrayList<>();
		ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
		scanner.addIncludeFilter(new AnnotationTypeFilter(Controller.class));
		scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));

		for (BeanDefinition bd : scanner.findCandidateComponents("org.gringlobal.brapi.v1")) {
			registerBrAPIMethods(bd);
		}

		supportedCalls = ListUtils.unmodifiableList(supportedCalls);
		supportedCalls.forEach(call -> {
			log.info("BrAPI {} {} {} {}", call.versions, call.methods, call.call, call.dataTypes);
		});
	}

	private void registerBrAPIMethods(BeanDefinition bd) throws ClassNotFoundException {
		Class<?> clazz = Class.forName(bd.getBeanClassName());

		String apiPrefix = "";
		if (clazz.isAnnotationPresent(RequestMapping.class)) {
			RequestMapping mapping = clazz.getAnnotation(RequestMapping.class);
			apiPrefix = mapping.value()[0];
		} else if (clazz.isAnnotationPresent(RestController.class)) {
			RestController mapping = clazz.getAnnotation(RestController.class);
			apiPrefix = mapping.value();
		}

		for (Method m : clazz.getDeclaredMethods()) {
			registerBrAPIMethod(apiPrefix, m);
		}
	}

	private void registerBrAPIMethod(String apiPrefix, Method m) {
		if (m.isAnnotationPresent(RequestMapping.class)) {
			RequestMapping mapping = m.getAnnotation(RequestMapping.class);
			supportedCalls.add(new Call(apiPrefix, mapping.value()[0], mapping.method(), mapping.produces()));

		} else if (m.isAnnotationPresent(GetMapping.class)) {
			GetMapping mapping = m.getAnnotation(GetMapping.class);
			supportedCalls.add(new Call(apiPrefix, mapping.value()[0], GET_MAPPING, mapping.produces()));

		} else if (m.isAnnotationPresent(PostMapping.class)) {
			PostMapping mapping = m.getAnnotation(PostMapping.class);
			supportedCalls.add(new Call(apiPrefix, mapping.value()[0], POST_MAPPING, mapping.produces()));

		} else if (m.isAnnotationPresent(DeleteMapping.class)) {
			DeleteMapping mapping = m.getAnnotation(DeleteMapping.class);
			supportedCalls.add(new Call(apiPrefix, mapping.value()[0], DELETE_MAPPING, mapping.produces()));

		} else if (m.isAnnotationPresent(PutMapping.class)) {
			PutMapping mapping = m.getAnnotation(PutMapping.class);
			supportedCalls.add(new Call(apiPrefix, mapping.value()[0], PUT_MAPPING, mapping.produces()));
		}
	}

	@GetMapping(value = "/calls", produces = { MediaType.APPLICATION_JSON_VALUE })
	public BrAPIResponse<Call> brapiCalls(final BrAPIPage page) {
		Pageable pageable = page.toPageRequest();
		Page<Call> callsPage = new PageImpl<>(callsPage(pageable), pageable, supportedCalls.size());
		return new BrAPIResponse<>(callsPage);
	}

	private List<Call> callsPage(Pageable page) {
		if (page.getOffset() >= supportedCalls.size()) {
			return Collections.emptyList();
		}
		return supportedCalls.subList((int) page.getOffset(), Math.min(supportedCalls.size(), (int) page.getOffset() + page.getPageSize()));
	}


	public static class Call {
		/** The Constant BRAPI_CALL_PREFIX. */
		private static final String BRAPI_CALL_PREFIX = "/brapi/v1/";

		/** The call. */
		public String call;

		/** The data types. */
		public String[] dataTypes;

		/** The methods. */
		public RequestMethod[] methods;

		/** The Constant versions. */
		private final String[] versions = { "1.3" };

		public Call(String apiPrefix, String endpoint, RequestMethod[] requestMethods, String[] dataTypes) {
			this.call = apiPrefix.concat(endpoint);
			if (this.call.startsWith(BRAPI_CALL_PREFIX)) {
				this.call = this.call.substring(BRAPI_CALL_PREFIX.length());
			}
			this.methods = requestMethods;
			this.dataTypes = dataTypes;
			if (this.dataTypes == null || this.dataTypes.length == 0) {
				this.dataTypes = new String[] { MediaType.APPLICATION_JSON_VALUE };
			}
		}

		@Override
		public String toString() {
			return "" + call + " " + Arrays.toString(methods) + " dataTypes=" + Arrays.toString(dataTypes);
		}
	}

}