Skip to content

Commit ac874e8

Browse files
committed
added postgresql connectivity, sample service & controller and test
1 parent 658cf73 commit ac874e8

19 files changed

Lines changed: 575 additions & 174 deletions

File tree

.github/workflows/main-build-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
3131

3232
- name: Build with Gradle
33-
run: ./gradlew build
33+
run: ./gradlew spotlessCheck build
3434

3535
# Set up BuildKit Docker container builder to be able to build
3636
# multi-platform images and export cache

HELP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,3 @@ The following guides illustrate how to use some features concretely:
2121
These additional references should also help you:
2222

2323
* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)
24-

build.gradle

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugins {
55
id 'jacoco'
66
id "org.sonarqube" version "7.2.1.6560"
77
id 'com.github.ben-manes.versions' version '0.53.0'
8+
id 'com.diffplug.spotless' version '6.25.0'
89
}
910

1011
group = 'com.generic'
@@ -49,16 +50,50 @@ dependencies {
4950
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14'
5051
implementation 'org.springframework.boot:spring-boot-starter-actuator'
5152
implementation 'org.springframework.boot:spring-boot-starter-web'
53+
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
5254
implementation 'org.springframework.boot:spring-boot-starter-validation'
5355
implementation 'io.hawt:hawtio-springboot:4.1.0'
5456
implementation 'net.logstash.logback:logstash-logback-encoder:9.0'
57+
runtimeOnly 'org.postgresql:postgresql'
5558
compileOnly 'org.projectlombok:lombok'
5659
annotationProcessor 'org.projectlombok:lombok'
5760
developmentOnly 'org.springframework.boot:spring-boot-devtools'
5861
testImplementation 'org.springframework.boot:spring-boot-starter-test'
62+
testImplementation 'org.springframework.security:spring-security-test'
5963
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
6064
}
6165

66+
spotless {
67+
// Defines the formatting rules for Java source files
68+
java {
69+
// Specify the file paths to check
70+
target 'src/main/java/**/*.java', 'src/test/java/**/*.java'
71+
72+
// Use Google Java Format (GJS) as the formatter
73+
// Use the default version included with Spotless, or specify one (e.g., '1.17.0')
74+
googleJavaFormat()
75+
76+
// Optionally, ensure that license headers are present at the top of every file
77+
// For example, enforcing a standard Apache 2.0 header:
78+
// licenseHeaderFile rootProject.file('LICENSE_HEADER.txt')
79+
80+
// This makes sure your imports are correctly ordered (recommended)
81+
// Spring Boot projects often benefit from the "removeUnusedImports" step.
82+
removeUnusedImports()
83+
}
84+
85+
// Optional: Add configuration for other file types common in Spring Boot projects
86+
format 'misc', {
87+
// For files like .md, .gitignore, .properties, etc.
88+
target '**/*.md', '**/.gitignore', '**/*.properties'
89+
90+
// Ensures files end with a newline character
91+
trimTrailingWhitespace()
92+
indentWithSpaces()
93+
endWithNewline()
94+
}
95+
}
96+
6297
tasks.named('test') {
6398
useJUnitPlatform()
6499
}

src/main/java/com/generic/server/ServerApplication.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
@SpringBootApplication
99
public class ServerApplication {
1010

11-
public static void main(String[] args) {
12-
log.info("Starting application");
13-
SpringApplication.run(ServerApplication.class, args);
14-
}
15-
11+
public static void main(String[] args) {
12+
log.info("Starting application");
13+
SpringApplication.run(ServerApplication.class, args);
14+
}
1615
}

src/main/java/com/generic/server/config/HttpConfig.java

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,52 @@
1212
@Configuration
1313
public class HttpConfig {
1414

15-
private static final String[] SWAGGER_PATHS = {
16-
"/swagger-ui/**",
17-
"/v3/api-docs/**"
18-
};
15+
private static final String[] SWAGGER_PATHS = {"/swagger-ui/**", "/v3/api-docs/**"};
1916

20-
private static final String[] ACTUATOR_CSRF_EXCLUDED_PATHS = {
21-
"/actuator/hawtio/**",
22-
"/actuator/jolokia/**",
23-
};
17+
private static final String[] ACTUATOR_CSRF_EXCLUDED_PATHS = {
18+
"/actuator/hawtio/**", "/actuator/jolokia/**",
19+
};
2420

21+
@Bean
22+
public SecurityFilterChain localLoginAndRedirectFilterChain(HttpSecurity http) throws Exception {
2523

26-
@Bean
27-
public SecurityFilterChain localLoginAndRedirectFilterChain(HttpSecurity http) throws Exception {
28-
29-
http.csrf(csrf -> csrf
30-
.csrfTokenRepository(new CookieCsrfTokenRepository())
24+
http.csrf(
25+
csrf ->
26+
csrf.csrfTokenRepository(new CookieCsrfTokenRepository())
3127
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
32-
.ignoringRequestMatchers(ACTUATOR_CSRF_EXCLUDED_PATHS)
33-
);
34-
http.authorizeHttpRequests(authorize -> authorize
35-
.requestMatchers(HttpMethod.GET, SWAGGER_PATHS).authenticated()
36-
.requestMatchers("/actuator/**").hasAnyRole("ACTUATOR", "ADMIN")
37-
.anyRequest().authenticated()
38-
);
39-
SavedRequestAwareAuthenticationSuccessHandler handler =
40-
new SavedRequestAwareAuthenticationSuccessHandler();
41-
handler.setDefaultTargetUrl("/actuator/hawtio");
42-
handler.setAlwaysUseDefaultTargetUrl(false);
43-
handler.setRedirectStrategy(new NoContinueRedirectStrategy());
44-
http.formLogin(formLogin -> formLogin
45-
.successHandler(handler)
46-
);
47-
http.httpBasic(httpBasic -> {});
48-
return http.build();
49-
}
50-
51-
static class NoContinueRedirectStrategy implements org.springframework.security.web.RedirectStrategy {
52-
org.springframework.security.web.DefaultRedirectStrategy delegate =
53-
new org.springframework.security.web.DefaultRedirectStrategy();
54-
@Override
55-
public void sendRedirect(
56-
jakarta.servlet.http.HttpServletRequest request,
57-
jakarta.servlet.http.HttpServletResponse response,
58-
String url
59-
) throws java.io.IOException {
60-
String cleanedUrl = url.replace("?continue", "").replace("&continue", "");
61-
delegate.sendRedirect(request, response, cleanedUrl);
62-
}
28+
.ignoringRequestMatchers(ACTUATOR_CSRF_EXCLUDED_PATHS));
29+
http.authorizeHttpRequests(
30+
authorize ->
31+
authorize
32+
.requestMatchers(HttpMethod.GET, SWAGGER_PATHS)
33+
.authenticated()
34+
.requestMatchers("/actuator/**")
35+
.hasAnyRole("ACTUATOR", "ADMIN")
36+
.anyRequest()
37+
.authenticated());
38+
SavedRequestAwareAuthenticationSuccessHandler handler =
39+
new SavedRequestAwareAuthenticationSuccessHandler();
40+
handler.setDefaultTargetUrl("/actuator/hawtio");
41+
handler.setAlwaysUseDefaultTargetUrl(false);
42+
handler.setRedirectStrategy(new NoContinueRedirectStrategy());
43+
http.formLogin(formLogin -> formLogin.successHandler(handler));
44+
http.httpBasic(httpBasic -> {});
45+
return http.build();
46+
}
47+
48+
static class NoContinueRedirectStrategy
49+
implements org.springframework.security.web.RedirectStrategy {
50+
org.springframework.security.web.DefaultRedirectStrategy delegate =
51+
new org.springframework.security.web.DefaultRedirectStrategy();
52+
53+
@Override
54+
public void sendRedirect(
55+
jakarta.servlet.http.HttpServletRequest request,
56+
jakarta.servlet.http.HttpServletResponse response,
57+
String url)
58+
throws java.io.IOException {
59+
String cleanedUrl = url.replace("?continue", "").replace("&continue", "");
60+
delegate.sendRedirect(request, response, cleanedUrl);
6361
}
64-
65-
}
62+
}
63+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.generic.server.config;
2+
3+
import javax.sql.DataSource;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.context.annotation.Primary;
7+
import org.springframework.jdbc.core.JdbcTemplate;
8+
9+
@Configuration
10+
public class SQLConfig {
11+
12+
/**
13+
* Spring Boot automatically provides a configured DataSource (HikariCP) based on the
14+
* 'spring.datasource' properties. We mark it @Primary as this is the main RDBMS connection.
15+
*/
16+
@Primary
17+
@Bean
18+
public JdbcTemplate postgresJdbcTemplate(DataSource dataSource) {
19+
return new JdbcTemplate(dataSource);
20+
}
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.generic.server.config;
2+
3+
import io.swagger.v3.oas.models.ExternalDocumentation;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.info.Contact;
6+
import io.swagger.v3.oas.models.info.Info;
7+
import io.swagger.v3.oas.models.info.License;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class SwaggerConfig {
13+
14+
@Bean
15+
public OpenAPI customOpenAPI() {
16+
return new OpenAPI()
17+
.info(
18+
new Info()
19+
.title("Generic Enterprise API")
20+
.version("v1.0.0")
21+
.description(
22+
"Enterprise API for core services, supporting PostgreSQL, Redis, and MongoDB.")
23+
.termsOfService("http://www.generic-enterprise.com/terms")
24+
.contact(
25+
new Contact()
26+
.name("Rohit Handique")
27+
.url("http://www.generic-enterprise.com/support")
28+
.email("rohit.handique@generic-enterprise.com"))
29+
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
30+
.externalDocs(
31+
new ExternalDocumentation()
32+
.description("Generic Server Documentation")
33+
.url("http://www.generic-enterprise.com/docs"));
34+
}
35+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.generic.server.controller;
2+
3+
import com.generic.server.model.entity.DatabaseTable;
4+
import com.generic.server.service.GenericService;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.media.Content;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import java.util.List;
11+
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
@Tag(
19+
name = "Metadata Management",
20+
description = "API for retrieving application and infrastructure metadata.")
21+
@RestController
22+
@RequestMapping(value = "/api/v1/metadata", produces = MediaType.APPLICATION_JSON_VALUE)
23+
public class GenericController {
24+
25+
private final GenericService genericService;
26+
27+
@Autowired
28+
public GenericController(GenericService genericService) {
29+
this.genericService = genericService;
30+
}
31+
32+
/**
33+
* Endpoint to retrieve all non-system tables from the PostgreSQL database. This method is
34+
* enhanced with OpenAPI annotations for Apigee consumption.
35+
*/
36+
@Operation(
37+
summary = "Retrieve Application Database Tables",
38+
description =
39+
"Fetches a list of all non-system tables defined in the PostgreSQL database schema. Useful for monitoring/diagnostics.",
40+
operationId = "getApplicationTables")
41+
@ApiResponse(
42+
responseCode = "200",
43+
description = "Successfully retrieved the list of database tables.",
44+
content =
45+
@Content(
46+
mediaType = MediaType.APPLICATION_JSON_VALUE,
47+
schema = @Schema(implementation = DatabaseTable.class)))
48+
@ApiResponse(
49+
responseCode = "500",
50+
description = "Internal Server Error. Connection failed or query execution error.",
51+
content =
52+
@Content(
53+
schema =
54+
@Schema(example = "{\"status\": 500, \"error\": \"Database connection error\"}")))
55+
@GetMapping("/tables")
56+
public ResponseEntity<List<DatabaseTable>> getAppTables() {
57+
List<DatabaseTable> tables = genericService.getApplicationTables();
58+
return ResponseEntity.ok().body(tables);
59+
}
60+
}

src/main/java/com/generic/server/filter/HttpFilter.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@
22

33
import jakarta.servlet.*;
44
import jakarta.servlet.http.*;
5+
import java.io.IOException;
56
import org.springframework.http.*;
67
import org.springframework.lang.NonNull;
78
import org.springframework.stereotype.Component;
89
import org.springframework.web.filter.OncePerRequestFilter;
910

10-
import java.io.IOException;
11-
1211
@Component
1312
public class HttpFilter extends OncePerRequestFilter {
14-
@Override
15-
protected void doFilterInternal(
16-
HttpServletRequest request,
17-
@NonNull HttpServletResponse response,
18-
@NonNull FilterChain filterChain) throws ServletException, IOException {
19-
String requestUri = request.getRequestURI();
20-
if (requestUri.endsWith("/") && !requestUri.equals("/")) {
21-
String newUrl = requestUri.substring(0, requestUri.length() - 1);
22-
response.setStatus(HttpStatus.PERMANENT_REDIRECT.value());
23-
response.setHeader(HttpHeaders.LOCATION, newUrl);
24-
return;
25-
}
26-
filterChain.doFilter(request, response);
13+
@Override
14+
protected void doFilterInternal(
15+
HttpServletRequest request,
16+
@NonNull HttpServletResponse response,
17+
@NonNull FilterChain filterChain)
18+
throws ServletException, IOException {
19+
String requestUri = request.getRequestURI();
20+
if (requestUri.endsWith("/") && !requestUri.equals("/")) {
21+
String newUrl = requestUri.substring(0, requestUri.length() - 1);
22+
response.setStatus(HttpStatus.PERMANENT_REDIRECT.value());
23+
response.setHeader(HttpHeaders.LOCATION, newUrl);
24+
return;
2725
}
28-
}
26+
filterChain.doFilter(request, response);
27+
}
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.generic.server.model.entity;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
/** A generic model class (DTO/Record) to hold the results of the table listing query. */
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Schema(
15+
description = "Represents a non-system table in the PostgreSQL database.",
16+
example =
17+
"{\"tableName\": \"users\", \"schemaName\": \"public\", \"tableOwner\": \"app_user\"}")
18+
public class DatabaseTable {
19+
20+
@Schema(description = "The name of the database table (e.g., 'orders').", example = "users")
21+
private String tableName;
22+
23+
@Schema(
24+
description = "The schema containing the table (e.g., 'public' or 'analytics').",
25+
example = "public")
26+
private String schemaName;
27+
28+
@Schema(
29+
description = "The PostgreSQL role that owns the table. Used for access control.",
30+
example = "app_service_account")
31+
private String tableOwner;
32+
}

0 commit comments

Comments
 (0)