UNCLASSIFIED

You need to sign in or sign up before continuing.
Commits (4)
#! bash
mvn -Dliquibase.url=jdbc:postgresql://localhost:5432/common_slim -Dliquibase.username=${USER} -Dliquibase.password='' -Pproduction liquibase:diff
...@@ -68,10 +68,6 @@ ...@@ -68,10 +68,6 @@
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.2</version> <version>2.5.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId> <groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId> <artifactId>jackson-dataformat-yaml</artifactId>
...@@ -159,23 +155,6 @@ ...@@ -159,23 +155,6 @@
<artifactId>json-patch</artifactId> <artifactId>json-patch</artifactId>
<version>1.12</version> <version>1.12</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>${camel.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.jayway.jsonpath</groupId> <groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId> <artifactId>json-path</artifactId>
...@@ -191,22 +170,6 @@ ...@@ -191,22 +170,6 @@
<artifactId>camel-spring-boot-starter</artifactId> <artifactId>camel-spring-boot-starter</artifactId>
<version>${camel.version}</version> <version>${camel.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jackson</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-servlet</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-http</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId> <artifactId>springdoc-openapi-data-rest</artifactId>
......
export DB_NAME='common_slim'
createdb ${DB_NAME}
if [[ "$1" == "clean" ]]; then if [[ "$1" == "clean" ]]; then
echo "Cleaning and re-creating db..."; echo "Cleaning and re-creating db...";
dropdb common; dropdb ${DB_NAME};
createdb common; createdb ${DB_NAME};
fi; fi;
export SPRING_PROFILES_ACTIVE=production; export SPRING_PROFILES_ACTIVE=production;
export SECURITY_ENABLED=true; export SECURITY_ENABLED=true;
export PGHOST='localhost'; export PGHOST='localhost';
export PGPORT='5432'; export PGPORT='5432';
export PG_DATABASE=common; export PG_DATABASE=${DB_NAME};
export APP_DB_ADMIN_PASSWORD=''; export APP_DB_ADMIN_PASSWORD='';
export PG_USER=${USER}; export PG_USER=${USER};
export APP_DB_RW_PASSWORD=''; export APP_DB_RW_PASSWORD='';
......
package mil.tron.commonapi;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
import lombok.Getter;
import lombok.Setter;
@Configuration
@ConfigurationProperties()
@Validated
public class ApplicationProperties {
@Getter
@Setter
private List<String> combinedPrefixes = new ArrayList<>();
@Getter
@Setter
@NotEmpty
private Map<String, @NotEmpty String> apiPrefix;
@Getter
@Setter
@NotEmpty
private String appSourcesPrefix;
@PostConstruct
private void createCombinedPrefixes() {
if(apiPrefix != null && !apiPrefix.isEmpty()) {
String appSourcePath = appSourcesPrefix == null ? "" : appSourcesPrefix;
this.combinedPrefixes.addAll(apiPrefix.values().stream().map(prefix -> {
if(prefix != null) {
return prefix.concat(appSourcePath);
}
return appSourcePath;
}).collect(Collectors.toList()));
}
}
}
package mil.tron.commonapi;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import mil.tron.commonapi.appgateway.GatewayCacheResolver;
import mil.tron.commonapi.appgateway.GatewayKeyGenerator;
@Configuration
@EnableCaching(order = 2147483647)
@ConditionalOnProperty(name = "caching.enabled")
@PropertySource("classpath:application.properties")
public class CacheConfig {
public static final String SERVICE_ENTITY_CACHE_MANAGER = "serviceEntityCacheManager";
public static final String APP_SOURCE_DETAILS_CACHE_NAME = "app_source_details_cache";
@Bean
public Caffeine<Object, Object> caffeineConfig(@Value("${caching.expire.time:10}") int expireTime, @Value("${caching.expire.unit:MINUTES}") String expireUnit) {
TimeUnit unit;
try {
unit = TimeUnit.valueOf(expireUnit);
} catch(IllegalArgumentException iaEx) {
unit = TimeUnit.MINUTES;
}
return Caffeine.newBuilder().expireAfterWrite(expireTime, unit);
}
@Primary
@Bean
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
@Bean
public CacheManager serviceEntityCacheManager() {
return new ConcurrentMapCacheManager(APP_SOURCE_DETAILS_CACHE_NAME);
}
@Bean("gatewayCacheResolver")
public CacheResolver cacheResolver() {
return new GatewayCacheResolver();
}
@Bean("gatewayKeyGenerator")
public KeyGenerator keyGenerator() {
return new GatewayKeyGenerator();
}
}
package mil.tron.commonapi; package mil.tron.commonapi;
import mil.tron.commonapi.entity.appsource.AppSource;
import mil.tron.commonapi.repository.appsource.AppSourceRepository;
import mil.tron.commonapi.service.AppSourceService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.CommonsRequestLoggingFilter; import org.springframework.web.filter.CommonsRequestLoggingFilter;
import java.time.Duration;
import java.util.List;
@SpringBootApplication @SpringBootApplication
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
...@@ -37,32 +27,4 @@ public class CommonApiApplication { ...@@ -37,32 +27,4 @@ public class CommonApiApplication {
return loggingFilter; return loggingFilter;
} }
/**
* Publisher-Subscriber REST bean that will timeout after 5secs to a subscriber so that
* a subscriber can't block/hang the publisher thread
* @param builder
* @return RestTemplate for use by the EventPublisher
*/
@Bean("eventSender")
public RestTemplate publisherSender(@Value("${webhook-send-timeout-secs:5}") long webhookSendTimeoutSecs,
RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(webhookSendTimeoutSecs))
.setReadTimeout(Duration.ofSeconds(webhookSendTimeoutSecs))
.build();
}
/**
* Launch the health check instances for each app source that's supposed to report status
*/
@Bean
public ApplicationRunner init(AppSourceService appSourceService, AppSourceRepository appSourceRepository) {
return (args) -> {
List<AppSource> appSources = appSourceRepository.findAll();
for (AppSource appSource : appSources) {
appSourceService.registerAppReporting(appSource);
}
};
}
} }
package mil.tron.commonapi;
import java.util.Date;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.step.StepMeterRegistry;
import io.micrometer.core.instrument.util.MeterPartition;
import io.micrometer.core.instrument.util.NamedThreadFactory;
import mil.tron.commonapi.service.MetricService;
public class CustomMeterRegistry extends StepMeterRegistry {
private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("gateway-metrics-publisher");
private CustomRegistryConfig config;
private MetricService metricService;
public CustomMeterRegistry(CustomRegistryConfig config, Clock clock, MetricService metricService) {
this(config, clock, DEFAULT_THREAD_FACTORY, metricService);
}
@Autowired
public CustomMeterRegistry(CustomRegistryConfig config, Clock clock, ThreadFactory threadFactory, MetricService metricService) {
super(config, clock);
this.config = config;
this.metricService = metricService;
start(threadFactory);
}
@Override
public void start(ThreadFactory threadFactory) {
super.start(threadFactory);
}
public CustomRegistryConfig getConfig() {
return config;
}
@Override
protected void publish() {
// partition into smaller chunks, because we might have a lot of stuff....
this.metricService.publishToDatabase(MeterPartition.partition(this, config.batchSize()), new Date(), this);
}
@Override
protected TimeUnit getBaseTimeUnit() {
return TimeUnit.MILLISECONDS;
}
}
\ No newline at end of file
package mil.tron.commonapi;
import io.micrometer.core.instrument.step.StepRegistryConfig;
public interface CustomRegistryConfig extends StepRegistryConfig {
CustomRegistryConfig DEFAULT = k -> null;
@Override
default String prefix() {
return "gateway";
}
}
\ No newline at end of file
package mil.tron.commonapi;
import java.time.Duration;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import mil.tron.commonapi.service.MetricService;
@Configuration
@ConditionalOnProperty(name = "metrics.save.enabled")
@PropertySource("classpath:application.properties")
public class MetricsConfig {
@Bean()
public CustomRegistryConfig customRegistryConfig(@Value("${metrics.stepsize:10}") int stepsize) {
return new CustomRegistryConfig(){
@Override
public Duration step() {
return Duration.ofMinutes(stepsize);
}
@Override
public String get(String key) {
return null;
}
};
}
@Bean()
public CompositeMeterRegistry compositeMeterRegistry(MetricService metricService, CustomRegistryConfig customRegistryConfig) {
return new CompositeMeterRegistry(Clock.SYSTEM, Arrays.asList(
new CustomMeterRegistry(customRegistryConfig, Clock.SYSTEM, metricService)
));
}
@Bean
public MeterFilter meterFilter() {
return MeterFilter.denyUnless((id) -> id.getName().startsWith("gateway"));
}
}
\ No newline at end of file
...@@ -24,14 +24,7 @@ public class SpringdocConfig { ...@@ -24,14 +24,7 @@ public class SpringdocConfig {
String[] paths = { String[] paths = {
String.format("%s/person/**", apiPrefix), String.format("%s/person/**", apiPrefix),
String.format("%s/organization/**", apiPrefix), String.format("%s/organization/**", apiPrefix),
String.format("%s/airman/**", apiPrefix),
String.format("%s/subscriptions/**", apiPrefix),
String.format("%s/flight/**", apiPrefix),
String.format("%s/group/**", apiPrefix),
String.format("%s/squadron/**", apiPrefix),
String.format("%s/wing/**", apiPrefix),
String.format("%s/userinfo/**", apiPrefix), String.format("%s/userinfo/**", apiPrefix),
String.format("%s/scratch/**", apiPrefix),
String.format("%s/version/**", apiPrefix), String.format("%s/version/**", apiPrefix),
}; };
...@@ -41,12 +34,10 @@ public class SpringdocConfig { ...@@ -41,12 +34,10 @@ public class SpringdocConfig {
@Bean @Bean
public GroupedOpenApi dashboardApi(@Value("${api-prefix.v1}") String apiPrefix) { public GroupedOpenApi dashboardApi(@Value("${api-prefix.v1}") String apiPrefix) {
String[] paths = { String[] paths = {
String.format("%s/app-client/**", apiPrefix),
String.format("%s/privilege/**", apiPrefix), String.format("%s/privilege/**", apiPrefix),
String.format("%s/logfile/**", apiPrefix), String.format("%s/logfile/**", apiPrefix),
String.format("%s/logs/**", apiPrefix), String.format("%s/logs/**", apiPrefix),
String.format("%s/dashboard-users/**", apiPrefix), String.format("%s/dashboard-users/**", apiPrefix),
String.format("%s/app-source/**", apiPrefix),
}; };
return GroupedOpenApi.builder().group("dashboard-api").pathsToMatch(paths).build(); return GroupedOpenApi.builder().group("dashboard-api").pathsToMatch(paths).build();
} }
...@@ -56,14 +47,7 @@ public class SpringdocConfig { ...@@ -56,14 +47,7 @@ public class SpringdocConfig {
String[] paths = { String[] paths = {
String.format("%s/person/**", apiPrefix), String.format("%s/person/**", apiPrefix),
String.format("%s/organization/**", apiPrefix), String.format("%s/organization/**", apiPrefix),
String.format("%s/airman/**", apiPrefix),
String.format("%s/subscriptions/**", apiPrefix),
String.format("%s/flight/**", apiPrefix),
String.format("%s/group/**", apiPrefix),
String.format("%s/squadron/**", apiPrefix),
String.format("%s/wing/**", apiPrefix),
String.format("%s/userinfo/**", apiPrefix), String.format("%s/userinfo/**", apiPrefix),
String.format("%s/scratch/**", apiPrefix),
}; };
return GroupedOpenApi.builder().group("common-api-v2").pathsToMatch(paths).build(); return GroupedOpenApi.builder().group("common-api-v2").pathsToMatch(paths).build();
......
package mil.tron.commonapi.annotation.efa;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Simple annotation to mark a field for the ValidateEntityAccess class
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ProtectedField {
}
package mil.tron.commonapi.annotation.pubsub;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Preauthorizes access to pub sub ledger/replay/event endpoints if requester is a:
* - APP_CLIENT_DEVELOPER of any app, or
* - any APP_CLIENT, or
* - a DASHBOARD_ADMIN
*/
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('APP_CLIENT_DEVELOPER') || hasAuthority('APP_CLIENT') || hasAuthority('DASHBOARD_ADMIN')")
public @interface PreAuthorizeAnyAppClientOrDeveloper {
}
package mil.tron.commonapi.annotation.pubsub;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* Authorizes new subscription if the request is from a:
* - DASHBOARD_ADMIN, or
* - Requesting entity is a registered APP_CLIENT itself, or
* - Requesting entity is an APP_CLIENT_DEVELOPER of a registered app client
*
* Authorizes update of an EXISTING subscription if the request is from a:
* - DASHBOARD_ADMIN, or
* - Requesting entity is a registered APP_CLIENT_DEVELOPER of the registered app client for whom the subscription exists, or
* -
*/
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(
// first see if the subscription even exists
"(@subscriberServiceImpl.subscriptionExists(#subscriber.getId()) ? " +
// if it does then authorize if req is from app client dev associated with subscription or
// req is from the app client itself (namespace in subscribers URI equals user (app) in request principal)
"@appClientUserServiceImpl.userIsAppClientDeveloperForAppSubscription(#subscriber.getId(), authentication.getName()) : " +
// otherwise, if subscription is new, then any APP_CLIENT or APP_CLIENT_DEVELOPER is permitted
"(hasAuthority('APP_CLIENT') || hasAuthority('APP_CLIENT_DEVELOPER'))) " +
// if we get here, then DASHBOARD_ADMIN always trumps all
"|| hasAuthority('DASHBOARD_ADMIN')")
public @interface PreAuthorizeSubscriptionCreation {
}
package mil.tron.commonapi.annotation.pubsub;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Authorizes subscription retrieval/removal/etc if the requester is from a:
* - DASHBOARD_ADMIN, or
* - Requesting entity is a registered APP_CLIENT itself associated with given 'id' from the controller, or
* - Requesting entity is an APP_CLIENT_DEVELOPER of a registered app client for whom the subscription of 'id' is for
*/
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(
// first see if the subscription even exists
"(@subscriberServiceImpl.subscriptionExists(#id) ? " +
// if it does then authorize if req is from app client dev associated with subscription or
// req is from the app client itself (namespace in subscribers URI equals user (app) in request principal)
"@appClientUserServiceImpl.userIsAppClientDeveloperForAppSubscription(#id, authentication.getName()) : " +
// otherwise, if subscription does not exist, then deny
"false )" +
// if we get here, then DASHBOARD_ADMIN always trumps all
"|| hasAuthority('DASHBOARD_ADMIN')")
public @interface PreAuthorizeSubscriptionOwner {
}
package mil.tron.commonapi.annotation.security;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.access.prepost.PreAuthorize;
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('DASHBOARD_ADMIN') || @accessCheck.check(#requestObject)")
public @interface PreAuthorizeGateway {
}
package mil.tron.commonapi.appgateway;
import org.apache.camel.CamelContext;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class AppGatewayRouteBuilder {
public static final String ROUTE_TYPE = "direct:";
public static final String APP_GATEWAY_ENDPOINT_ID = "app-gateway";
private CamelContext camelContext;
public AppGatewayRouteBuilder(CamelContext camelContext) {
this.camelContext = camelContext;
}
public GatewayRoute createGatewayRoute(String appSourcePath) {
final GatewayRoute route = new GatewayRoute(camelContext, appSourcePath);
try {
camelContext.addRoutes(route);
} catch (Exception e) {
log.warn("Could not create Camel route for the App Source: " + appSourcePath);
}
return route;
}
public static String generateAppSourceRouteUri(String appSourcePath) {
return ROUTE_TYPE + appSourcePath;
}
public static String generateAppSourceRouteId(String appSourcePath) {
return String.format("%s_%s", APP_GATEWAY_ENDPOINT_ID, appSourcePath);
}
}
package mil.tron.commonapi.appgateway;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import javax.transaction.Transactional;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import mil.tron.commonapi.entity.appsource.AppSource;
import mil.tron.commonapi.repository.appsource.AppSourceRepository;
@Service
@Slf4j
public class AppSourceConfig {
private Map<AppSourceInterfaceDefinition, AppSource> appSourceDefs;
private Map<String, AppSourceInterfaceDefinition> appSourcePathToDefinitionMap;
private AppSourceRepository appSourceRepository;
@Autowired
public AppSourceConfig(AppSourceRepository appSourceRepository,
@Value("${appsource.definition-file}") String appSourceDefFile) {
this.appSourceRepository = appSourceRepository;
this.appSourceDefs = new HashMap<>();
this.appSourcePathToDefinitionMap = new HashMap<>();
this.registerAppSources(this.parseAppSourceDefs(appSourceDefFile));
}
private AppSourceInterfaceDefinition[] parseAppSourceDefs(String configFile) {
ObjectMapper objectMapper = new ObjectMapper();
AppSourceInterfaceDefinition[] defs = null;
try (
InputStream in = getClass().getResourceAsStream(configFile);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
) {
defs = objectMapper.readValue(reader, AppSourceInterfaceDefinition[].class);
}
catch (IOException e) {
log.warn("Could not parse app source file config.");
}
catch (NullPointerException e) {
log.warn("Could not find resource file.", e);
}
return defs;
}
public Map<AppSourceInterfaceDefinition, AppSource> getAppSourceDefs() {
return this.appSourceDefs;
}
private void registerAppSources(AppSourceInterfaceDefinition[] appSourceDefs) {
// attempt adding
if (appSourceDefs != null) {
for (AppSourceInterfaceDefinition appDef : appSourceDefs) {
this.appSourceDefs.put(appDef, this.registerAppSource(appDef));
}
}
}
@Transactional
AppSource registerAppSource(AppSourceInterfaceDefinition appDef) {
AppSource appSource;
if (!this.appSourceRepository.existsByNameIgnoreCase(appDef.getName())) {
// add new app source
appSource = AppSource.builder()
.name(appDef.getName())
.openApiSpecFilename(appDef.getOpenApiSpecFilename())
.appSourcePath(appDef.getAppSourcePath())
.availableAsAppSource(true)
.nameAsLower(appDef.getName().toLowerCase())
.build();
} else {
appSource = this.appSourceRepository.findByNameIgnoreCaseWithEndpoints(appDef.getName());
// refresh the database to always be correct
appSource.setAvailableAsAppSource(true);
appSource.setOpenApiSpecFilename(appDef.getOpenApiSpecFilename());
appSource.setAppSourcePath(appDef.getAppSourcePath());
}
try {
this.appSourceRepository.save(appSource);
}
catch (Exception e) {
log.warn(String.format("Unable to add app source %s.", appDef.getName()), e);
}
return this.appSourceRepository.findByNameIgnoreCaseWithEndpoints(appDef.getName());
}
/**
* Adds a source def mapping to the map
* @param appSourcePath
* @param appDef
* @return True if the app def is added. False if the app source path is not added and was already defined.
*/
public boolean addAppSourcePathToDefMapping(String appSourcePath, AppSourceInterfaceDefinition appDef) {
if (this.appSourcePathToDefinitionMap.get(appSourcePath) == null) {
this.appSourcePathToDefinitionMap.put(appSourcePath, appDef);
return true;
}
return false;
}
public void clearAppSourceDefs() {
this.appSourcePathToDefinitionMap.clear();
}
public Map<String, AppSourceInterfaceDefinition> getPathToDefinitionMap() {
return this.appSourcePathToDefinitionMap;
}
}
package mil.tron.commonapi.appgateway;
import lombok.Value;
import org.springframework.web.bind.annotation.RequestMethod;
@Value
public class AppSourceEndpoint {
String path;
RequestMethod method;
}
package mil.tron.commonapi.appgateway;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import lombok.extern.slf4j.Slf4j;
import mil.tron.commonapi.ApplicationProperties;
import mil.tron.commonapi.controller.AppGatewayController;
import mil.tron.commonapi.entity.appsource.AppEndpoint;
import mil.tron.commonapi.entity.appsource.AppSource;
import mil.tron.commonapi.repository.appsource.AppEndpointRepository;
@Service
@Slf4j
public class AppSourceEndpointsBuilder {
private AppGatewayController queryController;
private RequestMappingHandlerMapping requestMappingHandlerMapping;
private AppSourceConfig appSourceConfig;
private AppEndpointRepository appEndpointRepository;
private ApplicationProperties versionProperties;
private AppGatewayRouteBuilder appGatewayRouteBuilder;
@Autowired
AppSourceEndpointsBuilder(RequestMappingHandlerMapping requestMappingHandlerMapping,
AppGatewayController queryController,
AppSourceConfig appSourceConfig,
AppEndpointRepository appEndpointRepository,
ApplicationProperties versionProperties,
AppGatewayRouteBuilder appGatewayRouteBuilder
) {
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
this.queryController = queryController;
this.appSourceConfig = appSourceConfig;
this.versionProperties = versionProperties;
this.appEndpointRepository = appEndpointRepository;
this.appGatewayRouteBuilder = appGatewayRouteBuilder;
this.createAppSourceEndpoints(this.appSourceConfig);
}
private void createAppSourceEndpoints(AppSourceConfig appSourceConfig) {
Map<AppSourceInterfaceDefinition, AppSource> appDefs = appSourceConfig.getAppSourceDefs();
if (appDefs.keySet().size() == 0) {
log.warn("No AppSource Definitions were found.");
return;
}
for (AppSourceInterfaceDefinition appDef : appDefs.keySet()) {
this.initializeWithAppSourceDef(appDef, appDefs.get(appDef));
}
}
public void initializeWithAppSourceDef(AppSourceInterfaceDefinition appDef, AppSource appSource) {
try {
List<AppSourceEndpoint> appSourceEndpoints = this.parseAppSourceEndpoints(appDef.getOpenApiSpecFilename());
boolean newMapping = this.appSourceConfig.addAppSourcePathToDefMapping(appDef.getAppSourcePath(),
appDef);
// Register Camel routes for each individual App Source
appGatewayRouteBuilder.createGatewayRoute(appDef.getAppSourcePath());
if (newMapping) {
for (AppSourceEndpoint appEndpoint: appSourceEndpoints) {
for(String prefix : this.versionProperties.getCombinedPrefixes()) {
this.addMapping(appDef.getAppSourcePath(), appEndpoint, prefix);
}
this.addEndpointToSource(appEndpoint, appSource);
}
}
setUnusedFlagOnEndpointsNotInSpec(appSourceEndpoints, appSource);
}
catch (FileNotFoundException e) {
log.warn(String.format("Endpoints for %s could not be loaded from %s. File not found.", appDef.getName(),
appDef.getOpenApiSpecFilename()), e);
}
catch (IOException e) {
log.warn(String.format("Endpoints for %s could not be loaded from %s", appDef.getName(),
appDef.getOpenApiSpecFilename()), e);
}
catch (NoSuchMethodException e) {
log.warn("Unable to map app source path to a controller handler.", e);
}
}
private void setUnusedFlagOnEndpointsNotInSpec(List<AppSourceEndpoint> appSourceEndpoints, AppSource appSource) {
List<AppEndpoint> unusedEndpoints = appSource.getAppEndpoints().parallelStream()
.filter(item -> appSourceEndpoints.stream()
.noneMatch(appSourceEndpoint -> item.getPath().equals(appSourceEndpoint.getPath()) && item.getMethod().equals(appSourceEndpoint.getMethod())))
.map(item -> {
item.setDeleted(true);
return item;
})
.collect(Collectors.toList());
appEndpointRepository.saveAll(unusedEndpoints);
}
public List<AppSourceEndpoint> parseAppSourceEndpoints(String openApiFilename) throws IOException {
Resource apiResource = new ClassPathResource("appsourceapis/" + openApiFilename);
SwaggerParseResult result = new OpenAPIV3Parser().readLocation(apiResource.getURI().toString(),
null, null);
return result.getOpenAPI().getPaths().entrySet().stream()
.map(path -> {
String pathString = path.getKey();
// get operations from paths.. .readOperations
return path.getValue().readOperationsMap()
// build pojo for path and operations
.keySet().stream().map(method -> new AppSourceEndpoint(pathString,
AppSourceEndpointsBuilder.convertMethod(method)))
.collect(Collectors.toList());
}).flatMap(List::stream).collect(Collectors.toList());
}
public static RequestMethod convertMethod(PathItem.HttpMethod swaggerHttpMethod) {
RequestMethod converted;
switch (swaggerHttpMethod) {
case GET:
converted = RequestMethod.GET;
break;
case POST:
converted = RequestMethod.POST;
break;
case PUT:
converted = RequestMethod.PUT;
break;
case DELETE:
converted = RequestMethod.DELETE;
break;
case PATCH:
converted = RequestMethod.PATCH;
break;
default:
converted = RequestMethod.GET;
break;
}
return converted;
}
private void addMapping(String appSourcePath, AppSourceEndpoint endpoint, String prefix) throws NoSuchMethodException {
RequestMappingInfo requestMappingInfo = RequestMappingInfo
.paths(String.format("%s/%s%s",
prefix,
appSourcePath,
endpoint.getPath()))
.methods(endpoint.getMethod())
.produces(MediaType.APPLICATION_JSON_VALUE)
.build();
if (endpoint.getMethod().equals(RequestMethod.GET)) {
requestMappingHandlerMapping.registerMapping(requestMappingInfo, queryController,
AppGatewayController.class.getDeclaredMethod("handleCachedRequests", HttpServletRequest.class, HttpServletResponse.class, Map.class));
} else {
requestMappingHandlerMapping.registerMapping(requestMappingInfo, queryController,
AppGatewayController.class.getDeclaredMethod("handleRequests", HttpServletRequest.class, HttpServletResponse.class, Map.class));
}
}
private void addEndpointToSource(AppSourceEndpoint endpoint, AppSource appSource) {
AppEndpoint appEndpoint = appEndpointRepository.findByAppSourceEqualsAndMethodEqualsAndPathEquals(appSource, endpoint.getMethod(), endpoint.getPath())
.orElse(AppEndpoint.builder()
.appSource(appSource)
.method(endpoint.getMethod())
.path(endpoint.getPath())
.build());
appEndpoint.setDeleted(false);
try {
this.appEndpointRepository.save(appEndpoint);
} catch (Exception e) {
log.warn(String.format("Unable to add endpoint to app source %s.", appSource.getName()), e);
}
}
}
package mil.tron.commonapi.appgateway;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class AppSourceInterfaceDefinition {
String name;
String openApiSpecFilename;
String sourceUrl;
String appSourcePath;
}