Create a Spring Boot REST API using Swagger/OpenAPI "Generate Swagger OpenAPI REST API documentation for Spring Boot application"

By: (plus.google.com) +David Herron; Date: July 31, 2017

Tags: OpenAPI » Swagger » Spring Boot

The Swagger tools, and the OpenAPI format, are an excellent way to document REST API's and even to generate client or server stub libraries to ease implementation. The technology serves two purposes -- a) standardized documentation for REST API's, b) generating code from API documentation in several programming languages. An OpenAPI file is fairly simple to write, you declare REST endpoints, describe the parameters and the request type, and then describe responses. It allows you to define complex object models that can be used either as input to a service, or its output.

Unfortunately the Swagger website doesn't have adequate documentation of using the tools. And it proved difficult to find clear straight-forward tutorials showing how to get started. Even the most powerful tool can be hampered if folks are unable to use it.

The following tutorial is a complete demonstration of, starting from scratch, developing a small Spring Boot service using OpenAPI and the Swagger tools. We show how to go from an OpenAPI spec to generated Spring Boot code, and also how to generate an OpenAPI spec from running Spring Boot code. There are several issues with the workflow of generating code from the OpenAPI spec. It's more effective to instead write the service code, and add in the annotations required for the Swagger tools to generate the OpenAPI spec for you.

With the OpenAPI spec it's easy to produce interactive API documentation that programmers can try out directly in their web browser.

The need behind this project is that I'm building a larger system that will require a database of information about photovoltaic solar panels and solar inverters. A database is available at (www.gosolarcalifornia.ca.gov) GoSolarCalifornia that seems to be collected under a project run by the State of California. They publish spreadsheets and CSV files listing data about both, and therefore it's a relatively simple task to convert that into a REST API based web service.

What we'll create is a Spring Boot based REST API accompanied by solar panel and inverter specifications in two SQL database tables. We'll start by using the H2 in-memory database but will also touch on a MySQL implementation.

The OpenAPI specification -- generating initial code

On the (swagger.io) swagger.io website it's recommended to write your API using OpenAPI, and then use swagger-codegen to generate server code. Generating Spring Boot code is fairly straightforward. The simplest way to install swagger-codegen is by downloading the Docker container, assuming you have Docker installed.

$ docker run --rm -v `pwd`:/local -v `pwd`/config.json:/local/config.json \
    swaggerapi/swagger-codegen-cli  \
    generate  --lang spring   -i /local/service.yaml \
            -c /local/config.json -o /local/src-spring

In case you don't know Docker, head over to (docker.com) docker.com. The two minute summary is it's an excellent method to package software into containers. The technology is based on Linux containerization techniques and is extremely light-weight.

In this case we're using the swaggerapi/swagger-codegen-cli container. As the name implies this has the swagger-codegen tool packaged as a command-line utility. The -v options mount the current directory into the container as /local and /local/config.json. The --rm option removes the container that's created by this command once the command exits, so that we automatically clean up after ourselves.

When this container is executed with run, it takes the subsequent options as the swagger-codegen parameters. The corresponding github project describes those options: (github.com) https://github.com/swagger-api/swagger-codegen

In this case we're saying to generate code for --lang spring (which means Spring Boot), from service.yaml using the configuration in config.json and outputting to src-spring.

The configuration I used is:

{
  "groupId":"com.davidherron.pv-inverters-modules",
  "artifactId":"pv-inverters-modules",
  "artifactVersion":"0.1.0",
  "library":"spring-mvc",
  "apiPackage": "com.davidherron.pv-inverters-modules.api",
  "modelPackage": "com.davidherron.pv-inverters-modules.model"
}

The OpenAPI specification

And then the OpenAPI specification (service.yaml) is:

swagger: '2.0'
info:
    description: |
        Supply solar PV module and inverter data from GoSolarCalifornia database
    version: "0.1.0"
    title: "Solar Module and Solar Inverter REST"
    contact:
        email: "david@davidherron.com"
schemes:
    - "http"
# basePath: /v0.1

paths:
    /modules:
        get:
            description: Query the local PV Modules database
            operationId: getModules
            parameters:
                - name: manufacturer
                  in: query
                  type: string
                  required: false
                  description: Match manufacturer name
                - name: model
                  in: query
                  type: string
                  required: false
                  description: Match manufacturer-assigned model number
                - name: minPmax
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Minimum of allowable Pmax range
                - name: maxPmax
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Maximum of allowable Pmax range
                - name: minVpmax
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Minimum of allowable Vpmax range
                - name: maxVpmax
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Maximum of allowable Vpmax range
                - name: minVoc
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Minimum of allowable Voc range
                - name: maxVoc
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Maximum of allowable Voc range
                - name: minIsc
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Minimum of allowable Isx range
                - name: maxIsc
                  in: query
                  type: number
                  format: float
                  required: false
                  description: Maximum of allowable Isc range
                - in: query
                  name: family
                  type: string
                  enum:
                      - monocrystalline
                      - polycrystalline
                      - thinfilm
                      - cigsthin
                      - hitsithin
                  required: false
                  description: Matches against the panel family
                - in: query
                  name: technology
                  type: string
                  enum:
                    - monocsi
                    - multicsi
                    - thinfilm
                  required: false
                  description: Matches against the panel technology
            responses:
                200:
                    description: OK
                    schema:
                        type: array
                        items:
                            $ref: "#/definitions/Module"
                404:
                    description: NOT FOUND
    /inverters:
        get:
            description: Query the local PV Inverters database
            operationId: getInverters
            parameters:
                - in: query
                  name: manufacturer
                  type: string
                  required: false
                  description: Match manufacturer name
                - in: query
                  name: model
                  type: string
                  required: false
                  description: Match manufacturer-assigned model number
                - in: query
                  name: minpower
                  type: number
                  format: float
                  required: false
                  description: Minimum power continuous rating
                - in: query
                  name: maxpower
                  type: number
                  format: float
                  required: false
                  description: Maximum power continuous rating
                - in: query
                  name: minvoltage
                  type: number
                  format: float
                  required: false
                  description: Minimum nominal voltage
                - in: query
                  name: maxvoltage
                  type: number
                  format: float
                  required: false
                  description: Maximum nominal voltage
                - in: query
                  name: hasmeter
                  type: string
                  enum:
                      - yes
                      - no
                  description: Has a built-in meter
                - in: query
                  name: microinverter
                  type: string
                  enum:
                      - yes
                      - no
                  description: Is a microinverter system
            responses:
                200:
                    description: OK
                    schema:
                        type: array
                        items:
                            $ref: "#/definitions/Inverter"
                404:
                    description: NOT FOUND

definitions:
    Module:
        type: object
        properties:
            manufacturer:
                type: string
                description: Manufacturer name
            model:
                type: string
                description: Manufacturer-assigned model number
            description:
                type: string
                description: Manufacturer's description
            BIPV:
                type: string
                description: Building Integrated PV
            nameplatePmax:
                type: integer
                description: Pmax
            PTC:
                type: number
                format: float
                description: |
                    PVUSA Test Conditions, which were developed to test and compare PV systems as part of the PVUSA (Photovoltaics for Utility Scale Applications) project. PTC are 1,000 Watts per square meter solar irradiance, 20 degrees C air temperature, and wind speed of 1 meter per second at 10 meters above ground level. PV manufacturers use Standard Test Conditions, or STC, to rate their PV products. STC are 1,000 Watts per square meter solar irradiance, 25 degrees C cell temperature, air mass equal to 1.5, and ASTM G173-03 standard spectrum. The PTC rating, which is lower than the STC rating, is generally recognized as a more realistic measure of PV output because the test conditions better reflect "real-world" solar and climatic conditions, compared to the STC rating. All ratings in the list are DC (direct current) watts.
            notes:
                type: string
                description: Manufacturer's notes
            nameplateVpmax:
                type: number
                format: float
                description: Voltage at Maximum Power
            nameplateIpmax:
                type: number
                format: float
                description: Current at Maximum Power (Ip max)
            nameplateVoc:
                type: number
                format: float
                description: Open Circuit Voltage (Voc)
            nameplateIsc:
                type: number
                format: float
                description: Short Circuit Current (Isc)
            averageNOCT:
                type: number
                format: float
                description: "Nominal Operating Cell Temperature. The temperature reached by open circuited cells in a PV module under the following conditions: 800 watts per square meter irradiance on cell surface, 20 degrees Celsius air temperature, 1 meter per second wind velocity, and open back side mounting"
            betaVoc:
                type: number
                format: float
                description: Temperature Coefficient of Open Circuit Voltage, percent per degrees Celsius (β Voc)
            betaVpmax:
                type: number
                format: float
                description: Temperature Coefficient of Voltage at Maximum Power, percent per degrees Celsius (β Vp max)
            alphaIsc:
                type: number
                format: float
                description: Temperature Coefficient of Short Circuit Current, percent per degrees Celsius (α Isc)
            alphaIpmax:
                type: number
                format: float
                description: Temperature Coefficient of Current at Maximum Power, percent per degrees Celsius (α Ip max)
            gammaPmax:
                type: number
                format: float
                description: Temperature Coefficient of Maximum Power, percent per degrees Celsius (γ Pmax)
            VPmaxlow:
                type: number
                format: float
                description: Voltage at Maximum Power and Low Irradiance (V Pmax, low)
            IPmaxlow:
                type: number
                format: float
                description: Current at Maximum Power and Low Irradiance (I Pmax, low)
            VPmaxNOCT:
                type: number
                format: float
                description: Voltage at Maximum Power and NOCT. (V Pmax, NOCT)
            IPmaxNOCT:
                type: number
                format: float
                description: Current at Maximum Power and NOCT (I Pmax, NOCT)
            sizeShortSide:
                type: number
                format: float
                description: Size on short side, in Meters
            sizeLongSide:
                type: number
                format: float
                description: Size on long side, in Meters
            geometricMultiplier:
                type: number
                format: float
                description: Geometric Multiplier
            ApertureArea:
                type: number
                format: float
                description: Aperture Area (A_c)
            NumberCellsSeries:
                type: number
                format: float
                description: Number cells in series
            NumberCellsParallel:
                type: number
                format: float
                description: Number of cells in parallel
            type:
                type: string
                description: Module type
            technology:
                type: string
                description: The sort of panel
            mounting:
                type: string
                description: How the module is mounted
            lastUpdate:
                type: string
                description: Last update
            cecListingDate:
                type: string
                description: Listing date with California Energy Commission
    Inverter:
        type: object
        properties:
            manufacturer:
                type: string
                description: Manufacturer name
            model:
                type: string
                description: Manufacturer-assigned model number
            description:
                type: string
                description: Manufacturer's description
            powerRating:
                type: number
                format: float
                description: Power Rating, Continuous at Unity Power Factor (kW)
            voltageNominalAC:
                type: number
                format: float
                description: Nominal Voltage (Vac)
            efficiency:
                type: number
                format: float
                description: Weighted Efficiency (%)
            UL1741SA:
                type: string
                description: "UL 1741 Supplement A Certification*** (Certification Date)"
            firmwareVersionTested:
                type: string
                description: Firmware Version Tested
            builtInMeter:
                type: string
                description: Built-In Meter
            microInverter:
                type: string
                description: Microinverter
            notes:
                type: string
                description: Manufacturer's notes
            nightTareLoss:
                type: string
                description: "Night Tare Loss (W)"
            powerRatingContinuous:
                type: number
                format: float
                description: "Power Rating, Continuous, 40 deg C (kW)"
            nightTareLoss40C:
                type: string
                description: "Night Tare Loss 40 deg C (W)"
            voltageMinDC:
                type: number
                format: float
                description: Voltage Minimum (Vdc)
            voltageNominalDC:
                type: number
                format: float
                description: Voltage Nominal (Vdc)
            voltageMaxDC:
                type: number
                format: float
                description: Voltage Maximum (Vdc)
            powerLevel10:
                type: number
                format: float
                description: Power Level 10% (kW)
            powerLevel20:
                type: number
                format: float
                description: Power Level 20% (kW)
            powerLevel30:
                type: number
                format: float
                description: Power Level 30% (kW)
            powerLevel50:
                type: number
                format: float
                description: Power Level 50% (kW)
            powerLevel75:
                type: number
                format: float
                description: Power Level 75% (kW)
            powerLevel100:
                type: number
                format: float
                description: Power Level 100% (kW)
            efficiencyVmin10:
                type: number
                format: float
                description: Efficiency @Vmin, 10% (%)
            efficiencyVmin20:
                type: number
                format: float
                description: Efficiency @Vmin, 20% (%)
            efficiencyVmin30:
                type: number
                format: float
                description: Efficiency @Vmin, 30% (%)
            efficiencyVmin50:
                type: number
                format: float
                description: Efficiency @Vmin, 50% (%)
            efficiencyVmin75:
                type: number
                format: float
                description: Efficiency @Vmin, 75% (%)
            efficiencyVmin100:
                type: number
                format: float
                description: Efficiency @Vmin, 100% (%)
            efficiencyVnom10:
                type: number
                format: float
                description: Efficiency @Vnom, 10% (%)
            efficiencyVnom20:
                type: number
                format: float
                description: Efficiency @Vnom, 20% (%)
            efficiencyVnom30:
                type: number
                format: float
                description: Efficiency @Vnom, 30% (%)
            efficiencyVnom50:
                type: number
                format: float
                description: Efficiency @Vnom, 50% (%)
            efficiencyVnom75:
                type: number
                format: float
                description: Efficiency @Vnom, 75% (%)
            efficiencyVnom100:
                type: number
                format: float
                description: Efficiency @Vnom, 100% (%)
            efficiencyVmax10:
                type: number
                format: float
                description: Efficiency @Vmax, 10% (%)
            efficiencyVmax20:
                type: number
                format: float
                description: Efficiency @Vmax, 20% (%)
            efficiencyVmax30:
                type: number
                format: float
                description: Efficiency @Vmax, 30% (%)
            efficiencyVmax50:
                type: number
                format: float
                description: Efficiency @Vmax, 50% (%)
            efficiencyVmax75:
                type: number
                format: float
                description: Efficiency @Vmax, 75% (%)
            efficiencyVmax100:
                type: number
                format: float
                description: Efficiency @Vmax, 100% (%)
            sortOrder:
                type: string
                description: Sort order
            cecListingDate:
                type: string
                description: Listing date with California Energy Commission
            lastUpdate:
                type: string
                description: Last update

This describes two REST methods:

  • /modules queries the PV modules table. A number of optional parameters are specified letting us search in several different ways.
  • /inverters queries the inverters table, again with a number of optional parameters

The definitions section describes the object models. These models are derived directly from the CSV files provided by GoSolarCalifornia. The field descriptions are based partly from my experience with the technology, and partly from the GoSolarCalifornia documentation.

Examining the generated code

This generates the following:

$ tree src-spring
src-spring
├── README.md
├── pom.xml
└── src
    └── main
        ├── java
        │   ├── com
        │   │   └── davidherron
        │   │       └── pv_inverters_modules
        │   │           ├── api
        │   │           │   ├── ApiException.java
        │   │           │   ├── ApiOriginFilter.java
        │   │           │   ├── ApiResponseMessage.java
        │   │           │   ├── InvertersApi.java
        │   │           │   ├── InvertersApiController.java
        │   │           │   ├── ModulesApi.java
        │   │           │   ├── ModulesApiController.java
        │   │           │   └── NotFoundException.java
        │   │           └── model
        │   │               ├── Inverter.java
        │   │               └── Module.java
        │   └── io
        │       └── swagger
        │           └── configuration
        │               ├── CustomInstantDeserializer.java
        │               ├── RFC3339DateFormat.java
        │               ├── SwaggerDocumentationConfig.java
        │               ├── SwaggerUiConfiguration.java
        │               ├── WebApplication.java
        │               └── WebMvcConfiguration.java
        └── resources
            └── swagger.properties

12 directories, 19 files

It's not a complete Spring application. You have an api package containing Controller classes, and a model package containing Model classes. The generated code is a shell where you're meant to fill in details. It doesn't even have an Application class that's supposed to be the Spring Boot entry point.

For example, in InvertersApiController.java you'll find this method:

public ResponseEntity<List<Inverter>> getInverters(@ApiParam(value = "Match manufacturer name") @RequestParam(value = "manufacturer", required = false) String manufacturer,
    @ApiParam(value = "Match manufacturer-assigned model number") @RequestParam(value = "model", required = false) String model,
    @ApiParam(value = "Minimum power continuous rating") @RequestParam(value = "minpower", required = false) Float minpower,
    @ApiParam(value = "Maximum power continuous rating") @RequestParam(value = "maxpower", required = false) Float maxpower,
    @ApiParam(value = "Minimum nominal voltage") @RequestParam(value = "minvoltage", required = false) Float minvoltage,
    @ApiParam(value = "Maximum nominal voltage") @RequestParam(value = "maxvoltage", required = false) Float maxvoltage,
    @ApiParam(value = "Has a built-in meter", allowableValues = "true, false") @RequestParam(value = "hasmeter", required = false) String hasmeter,
    @ApiParam(value = "Is a microinverter system", allowableValues = "true, false") @RequestParam(value = "microinverter", required = false) String microinverter,
    @RequestHeader(value = "Accept", required = false) String accept) throws Exception {
    // do some magic!

    if (accept != null && accept.contains("application/json")) {
        return new ResponseEntity<List<Inverter>>(objectMapper.readValue("[ {  \"firmwareVersionTested\" : \"aeiou\",  \"efficiencyVnom20\" : 9.965781,  \"notes\" : \"aeiou\",  \"voltageNominalAC\" : 6.0274563,  \"description\" : \"aeiou\",  \"efficiencyVmin10\" : 1.0246457,  \"efficiencyVmin30\" : 6.846853,  \"efficiencyVmin75\" : 1.1730742,  \"efficiencyVmin50\" : 7.4577446,  \"manufacturer\" : \"aeiou\",  \"powerRating\" : 0.8008282,  \"builtInMeter\" : \"aeiou\",  \"nightTareLoss40C\" : \"aeiou\",  \"powerLevel20\" : 3.6160767,  \"UL1741SA\" : \"aeiou\",  \"voltageNominalDC\" : 2.302136,  \"voltageMinDC\" : 5.637377,  \"efficiencyVmax50\" : 1.284659,  \"model\" : \"aeiou\",  \"microInverter\" : \"aeiou\",  \"efficiencyVmax10\" : 6.4384236,  \"efficiencyVmax75\" : 2.8841622,  \"efficiencyVmax30\" : 6.965118,  \"efficiencyVnom10\" : 5.025005,  \"efficiency\" : 1.4658129,  \"efficiencyVnom75\" : 8.762042,  \"efficiencyVnom30\" : 9.36931,  \"efficiencyVnom50\" : 6.6835623,  \"voltageMaxDC\" : 7.0614014,  \"powerRatingContinuous\" : 5.962134,  \"efficiencyVnom100\" : 9.018348,  \"efficiencyVmin20\" : 1.4894159,  \"powerLevel30\" : 2.027123,  \"powerLevel75\" : 7.386282,  \"powerLevel10\" : 9.301444,  \"cecListingDate\" : \"aeiou\",  \"powerLevel100\" : 1.2315135,  \"powerLevel50\" : 4.145608,  \"sortOrder\" : \"aeiou\",  \"lastUpdate\" : \"aeiou\",  \"efficiencyVmax100\" : 6.778325,  \"nightTareLoss\" : \"aeiou\",  \"efficiencyVmin100\" : 4.9652185,  \"efficiencyVmax20\" : 3.5571952} ]", List.class), HttpStatus.OK);
    }

    return new ResponseEntity<List<Inverter>>(HttpStatus.OK);
}

This has both @ApiParam and @RequestParam annotations for each function parameter. The first is used by the Swagger libraries so that Swagger can generate API documentation, and the second is used by JavaEE/Spring/etc to pull in the parameter from the REST. You'll notice that this is missing a couple annotations like @RequestMapping. That's because the generated code implements an interface, and inside that interface you have this corresponding declaration:

@ApiOperation(value = "", notes = "Query the local PV Inverters database", response = Inverter.class, responseContainer = "List", tags={  })
@ApiResponses(value = {
    @ApiResponse(code = 200, message = "OK", response = Inverter.class, responseContainer = "List"),
    @ApiResponse(code = 404, message = "NOT FOUND", response = Void.class) })
@RequestMapping(value = "/inverters",
    method = RequestMethod.GET)
ResponseEntity<List<Inverter>> getInverters(
    @ApiParam(value = "Match manufacturer name") @RequestParam(value = "manufacturer", required = false) String manufacturer,
    @ApiParam(value = "Match manufacturer-assigned model number") @RequestParam(value = "model", required = false) String model,
    @ApiParam(value = "Minimum power continuous rating") @RequestParam(value = "minpower", required = false) Float minpower,
    @ApiParam(value = "Maximum power continuous rating") @RequestParam(value = "maxpower", required = false) Float maxpower,
    @ApiParam(value = "Minimum nominal voltage") @RequestParam(value = "minvoltage", required = false) Float minvoltage,
    @ApiParam(value = "Maximum nominal voltage") @RequestParam(value = "maxvoltage", required = false) Float maxvoltage,
    @ApiParam(value = "Has a built-in meter", allowableValues = "true, false") @RequestParam(value = "hasmeter", required = false) String hasmeter,
    @ApiParam(value = "Is a microinverter system", allowableValues = "true, false") @RequestParam(value = "microinverter", required = false) String microinverter,
    @RequestHeader(value = "Accept", required = false) String accept)
     throws Exception;

We have code, now what? The code generated by Swagger-Codegen isn't exactly useful

We went over the previous steps because this is the approach recommended by the Swagger team. However, as I worked with the code they generated I ran into a couple problems.

  • I don't agree with creating an interface and having the Controller class implement the interface. Doing so needlessly creates a second Class object that complicates the code base.
  • Changing the OpenAPI specification means regenerating the code and losing changes you've made. This issue is almost completely a show stopper.
  • It's not a complete Spring Boot application, and I'd rather start from code generated by the Spring Boot Initializr.

The other paradigm: Generating OpenAPI documentation from working Spring code

If you hunt for tutorial videos or blog posts about using Swagger or OpenAPI with Spring, you find a different recommendation. The universal recommendation is, instead of generating Spring code from the OpenAPI spec, to generate the OpenAPI spec from the Spring code.

In other words, you write your Spring application and then sprinkle in Swagger/SpringFox annotations. You then use the Swagger runtime to generate the JSON form of the OpenAPI spec. Your OpenAPI spec is always in sync with the code, because it's generated FROM the code. There's no concern with overwriting code, since you're not generating code from the spec.

We'll use an amalgam of the two paradigms in this posting. It's not that hard to write an OpenAPI spec, and it's a good exercise so your team can review and agree to the API before you commit it to code. As we'll see in a minute, we can use the code generated previously in the code generated by the Spring Initializr. And we'll we'll be able to check the generated OpenAPI spec against the hand-written version as a way to check our code.

An alternate -- Spring REST-Docs

(www.youtube.com) https://www.youtube.com/watch?v=i8TDt7cUG3k -- This presentation at the "Spring One Platform 2016" conference makes a fairly comprehensive critique of the Swagger/OpenAPI and presents Spring REST-Docs as an excellent alternative. He makes some very good points. A big flaw is that the approach he describes requires that you code in Spring. If you desire to code in other languages, the Spring REST-Docs approach obviously will not work.

Spring Initializr

Go to :- (start.spring.io) http://start.spring.io/ Then switch to the full version using the link at the bottom of the page which you can barely see.

Fill in the metadata section as desired.

Below that you select the starter kits. This simplifies starting a Spring Boot application because they've developed for you pre-configured Maven dependency selections.

For this project: Web, JDBC, H2, MySQL

Click on the Generate Project button, and it'll download some code for you. THIS is a complete running application which we can modify as we desire.

What we receive is:

$ tree ~/Downloads/pv_inverters_modules
/Users/david/Downloads/pv_inverters_modules
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── davidherron
    │   │           └── pv_inverters_modules
    │   │               └── PvInvertersModulesApplication.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── java
            └── com
                └── davidherron
                    └── pv_inverters_modules
                        └── PvInvertersModulesApplicationTests.java

14 directories, 6 files

This gives us the Application class, a Properties file, a POM, and clearly we need to merge these two bits of code plus implement some other code. For example, the Service and Repository classes that sit between the Controller and the Model classes.

Additions to pom.xml

Adding Swagger to our application is as simple as adding these dependencies:

<!--SpringFox dependencies --><!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.7.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.7.0</version>
</dependency>

As long as we're in this file, also add this dependency:

<!-- https://mvnrepository.com/artifact/net.sf.supercsv/super-csv -->
<dependency>
	<groupId>net.sf.supercsv</groupId>
	<artifactId>super-csv</artifactId>
	<version>2.4.0</version>
</dependency>

We'll use this to load the CSV files into the in-memory database.

Application properties file, and the H2 database

H2 is an in-memory SQL database. It works great with JDBC, supporting an SQL schema and SQL queries, and it lets us avoid the administrative overhead of configuring a regular SQL database server like MySQL.

I prefer using YAML instead of the properties file format. If you rename application.properties to application.yaml, Spring will automagically recognize the YAML file as if it were a Properties file.

In application.yaml add this:

spring:
  datasource:
    name: modules
    platform: h2
  h2:
    console:
      enabled: true
      path: /console

pvdata:
  inverters: Inverter_List_Full_Data.csv
  modules: PV_Module_List_Full_Data.csv

The YAML object structure corresponds directly to whatever would be documented for a Properties file. When you're told to enter spring.datasource.platform=h2 in the Properties file, simply translate it into this structure. The first segment of this file sets up the H2 engine, as well as the H2 console.

The second segment gives the name of the CSV files we download from the GoSolarCalifornia website.

Downloading CSV files

Go to: (www.gosolarcalifornia.ca.gov) http://www.gosolarcalifornia.ca.gov/equipment/index.php

The page has a link for PV Modules and for Inverters. Go to each of these pages and download the Full Data spreadsheet. Notice that they only provide Excel spreadsheet files.

Open the spreadsheets in your preferred Spreadsheet program. I use Libre Office, but I believe Google Sheets will also work. Once opened, export the data to CSV files as named above.

There is a problem with the exported CSV files. At least one of the Excel spreadsheets has a multi-row header above the data. The SuperCSV library is able to identify columns by the column name in the first row of the CSV file. But, column-recognition doesn't work when there's two or more header rows. Further, the header values they supply aren't very good.

Edit Inverter_List_Full_Data.csv and edit the header row(s) to be this:

ManufacturerName,ModelNumber,Description,PowerRatingContinuous,NominalVoltage,WeightedEfficiency,UL1741SupplementACertification,FirmwareVersionTested,BuiltInMeter,Microinverter,Notes,NightTareLoss,PowerRatingContinuous40degC,NightTareLoss40degC,VoltageMinimum,VoltageNominal,VoltageMaximum,PowerLevel10,PowerLevel20,PowerLevel30,PowerLevel50,PowerLevel75,PowerLevel100,EfficiencyVmin10,EfficiencyVmin20,EfficiencyVmin30,EfficiencyVmin50,EfficiencyVmin75,EfficiencyVmin100,EfficiencyVmin,EfficiencyVnom10,EfficiencyVnom20,EfficiencyVnom30,EfficiencyVnom50,EfficiencyVnom75,EfficiencyVnom100,EfficiencyVnom,EfficiencyVmax10,EfficiencyVmax20,EfficiencyVmax30,EfficiencyVmax50,EfficiencyVmax75,EfficiencyVmax100,EfficiencyVmax,SortOrder,CECListingDate,LastUpdate

Edit PV_Module_List_Full_Data.csv and edit the header row(s) to be this:

Manufacturer,ModelNumber,Description,BIPV,NameplatePmax,PTC,Notes,NameplateVpmax,NameplateIpmax,NameplateVoc,NameplateIsc,AverageNOCT,betaVoc,betaVpmax,alphaIsc,alphaIpmax,gammaPmax,VPmaxlow,IPmaxlow,VPmaxNOCT,IPmaxNOCT,ShortSide,LongSide,GeometricMultiplier,ApertureArea,NumberCellsSeries,NumberCellsParallel,Type,Family,Technology,P2Pref,PercentNonlin,a_ref,I_o_ref,I_L_ref,R_s_ref,R_sh_ref,delta,I_sc_adj,Time,Version,Mounting,LastUpdate,CECListingDate

Save the CSV files into src/main/resources using those file names.

Database schema

In src/main/resources create a file named schema-h2.sql containing:

DROP TABLE IF EXISTS inverters;

CREATE TABLE `inverters` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `ManufacturerName` text,
  `ModelNumber` text,
  `Description` text,
  `PowerRatingContinuous` double,
  `NominalVoltage` text,
  `WeightedEfficiency` double,
  `UL1741SupplementACertification` text,
  `FirmwareVersionTested` text,
  `BuiltInMeter` text,
  `Microinverter` text,
  `Notes` text,
  `NightTareLoss` double,
  `PowerRatingContinuous40degC` double,
  `NightTareLoss40degC` double,
  `VoltageMinimum` double,
  `VoltageNominal` double,
  `VoltageMaximum` double,
  `PowerLevel10` double,
  `PowerLevel20` double,
  `PowerLevel30` double,
  `PowerLevel50` double,
  `PowerLevel75` double,
  `PowerLevel100` double,
  `EfficiencyVmin10` double,
  `EfficiencyVmin20` double,
  `EfficiencyVmin30` double,
  `EfficiencyVmin50` double,
  `EfficiencyVmin75` double,
  `EfficiencyVmin100` double,
  `EfficiencyVmin` double,
  `EfficiencyVnom10` double,
  `EfficiencyVnom20` double,
  `EfficiencyVnom30` double,
  `EfficiencyVnom50` double,
  `EfficiencyVnom75` double,
  `EfficiencyVnom100` double,
  `EfficiencyVnom` double,
  `EfficiencyVmax10` double,
  `EfficiencyVmax20` double,
  `EfficiencyVmax30` double,
  `EfficiencyVmax50` double,
  `EfficiencyVmax75` double,
  `EfficiencyVmax100` double,
  `EfficiencyVmax` double,
  `SortOrder` text,
  `CECListingDate` text,
  `LastUpdate` text,
  PRIMARY KEY (id)
);

DROP TABLE IF EXISTS modules;

CREATE TABLE `modules` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `Manufacturer` text,
  `ModelNumber` text,
  `Description` text,
  `BIPV` text,
  `NameplatePmax` double,
  `PTC` double,
  `Notes` text,
  `NameplateVpmax` double,
  `NameplateIpmax` double,
  `NameplateVoc` double,
  `NameplateIsc` double,
  `AverageNOCT` double,
  `betaVoc` double,
  `betaVpmax` double,
  `alphaIsc` double,
  `alphaIpmax` double,
  `gammaPmax` double,
  `VPmaxlow` double,
  `IPmaxlow` double,
  `VPmaxNOCT` double,
  `IPmaxNOCT` double,
  `ShortSide` double,
  `LongSide` double,
  `GeometricMultiplier` text,
  `ApertureArea` double,
  `NumberCellsSeries` double,
  `NumberCellsParallel` double,
  `Type` text,
  `Family` text,
  `Technology` text,
  `P2Pref` double,
  `PercentNonlin` text,
  `a_ref` text,
  `I_o_ref` text,
  `I_L_ref` text,
  `R_s_ref` text,
  `R_sh_ref` text,
  `delta` text,
  `I_sc_adj` text,
  `Time` text,
  `Version` text,
  `Mounting` text,
  `LastUpdate` text,
  `CECListingDate` text,
  PRIMARY KEY (id)
);

When Spring starts the application, it will automatically run this SQL file and generate the database tables.

Loading data from CSV to H2 SQL tables

My preferred CSV library for Java is SuperCSV. We already added its dependency to the POM.

JavaDocs: (super-csv.github.io) https://super-csv.github.io/super-csv/apidocs/index.html

Home Page: (super-csv.github.io) http://super-csv.github.io/super-csv/index.html

Create a package to hold Service classes, such as com.davidherron.pvmodulesinverters.service, and create a class named DataLoader

Start with:

@Service
public class DataLoader {

    private static final Logger logger = LoggerFactory.getLogger(DataLoader.class);

    @Value("${pvdata.inverters}")
    private String pvDataInvertersCSV;

    @Value("${pvdata.modules}")
    private String pvDataModulesCSV;

    private InvertersRepository invertersRepository;
    private ModulesRepository modulesRepository;

    @Autowired
    public DataLoader(InvertersRepository invertersRepository, ModulesRepository modulesRepository) {
        this.invertersRepository = invertersRepository;
        this.modulesRepository = modulesRepository;
    }

}

This is a Service class. It brings in the InvertersRepository and ModulesRepository classes because we'll be saving data from the CSV files into those database tables. Since we haven't defined those classes, your IDE will complain about missing classes. We'll get to that later. The constructor is written to @Autowire these Repository classes.

We're about to write the code to use SuperCSV to read the CSV files.

SuperCSV works by creating a CellProcessor array corresponding to each CSV file. That means we need two such arrays. A CellProcessor array describes the fields of the CSV file, and how to process the column data into a Java object.

private static CellProcessor[] getInverterProcessors() {
    final CellProcessor[] processors = new CellProcessor[] {
        new Optional(), // ManufacturerName
        new Optional(), // ModelNumber
        new Optional(), // Description
        new Optional(new ParseDouble()), // PowerRatingContinuous
        new Optional(), // NominalVoltage
        new Optional(new ParseDouble()), // WeightedEfficiency
        new Optional(), // UL1741SupplementACertification
        new Optional(), // FirmwareVersionTested
        new Optional(), // BuiltInMeter
        new Optional(), // Microinverter
        new Optional(), // Notes
        new Optional(new ParseDouble()), // NightTareLoss
        new Optional(new ParseDouble()), // PowerRatingContinuous40degC
        new Optional(new ParseDouble()), // NightTareLoss40degC
        new Optional(new ParseDouble()), // VoltageMinimum
        new Optional(new ParseDouble()), // VoltageNominal
        new Optional(new ParseDouble()), // VoltageMaximum
        new Optional(new ParseDouble()), // PowerLevel10
        new Optional(new ParseDouble()), // PowerLevel20
        new Optional(new ParseDouble()), // PowerLevel30
        new Optional(new ParseDouble()), // PowerLevel50
        new Optional(new ParseDouble()), // PowerLevel75
        new Optional(new ParseDouble()), // PowerLevel100
        new Optional(new ParseDouble()), // EfficiencyVmin10
        new Optional(new ParseDouble()), // EfficiencyVmin20
        new Optional(new ParseDouble()), // EfficiencyVmin30
        new Optional(new ParseDouble()), // EfficiencyVmin50
        new Optional(new ParseDouble()), // EfficiencyVmin75
        new Optional(new ParseDouble()), // EfficiencyVmin100
        new Optional(new ParseDouble()), // EfficiencyVmin
        new Optional(new ParseDouble()), // EfficiencyVnom10
        new Optional(new ParseDouble()), // EfficiencyVnom20
        new Optional(new ParseDouble()), // EfficiencyVnom30
        new Optional(new ParseDouble()), // EfficiencyVnom50
        new Optional(new ParseDouble()), // EfficiencyVnom75
        new Optional(new ParseDouble()), // EfficiencyVnom100
        new Optional(new ParseDouble()), // EfficiencyVnom
        new Optional(new ParseDouble()), // EfficiencyVmax10
        new Optional(new ParseDouble()), // EfficiencyVmax20
        new Optional(new ParseDouble()), // EfficiencyVmax30
        new Optional(new ParseDouble()), // EfficiencyVmax50
        new Optional(new ParseDouble()), // EfficiencyVmax75
        new Optional(new ParseDouble()), // EfficiencyVmax100
        new Optional(new ParseDouble()), // EfficiencyVmax
        new Optional(), // SortOrder
        new Optional(), // CECListingDate
        new Optional() // LastUpdate
    };
    return processors;
}

This defines the CellProcessors for the Inverters CSV files. Most of the fields are floating point numbers, and I'd have preferred to use the Float type but SuperCSV only provides a Processor class for the Double type. These fields should match the columns in the database.

private static CellProcessor[] getModuleProcessors() {
    final CellProcessor[] processors = new CellProcessor[] {
        new Optional(), // Manufacturer
        new Optional(), // ModelNumber
        new Optional(), // Description
        new Optional(), // BIPV
        new Optional(new ParseDouble()), // NameplatePmax
        new Optional(new ParseDouble()), // PTC
        new Optional(), // Notes
        new Optional(new ParseDouble()), // NameplateVpmax
        new Optional(new ParseDouble()), // NameplateIpmax
        new Optional(new ParseDouble()), // NameplateVoc
        new Optional(new ParseDouble()), // NameplateIsc
        new Optional(new ParseDouble()), // AverageNOCT
        new Optional(new ParseDouble()), // betaVoc
        new Optional(new ParseDouble()), // betaVpmax
        new Optional(new ParseDouble()), // alphaIsc
        new Optional(new ParseDouble()), // alphaIpmax
        new Optional(new ParseDouble()), // gammaPmax
        new Optional(new ParseDouble()), // VPmaxlow
        new Optional(new ParseDouble()), // IPmaxlow
        new Optional(new ParseDouble()), // VPmaxNOCT
        new Optional(new ParseDouble()), // IPmaxNOCT
        new Optional(new ParseDouble()), // ShortSide
        new Optional(new ParseDouble()), // LongSide
        new Optional(new ParseDouble()), // GeometricMultiplier
        new Optional(new ParseDouble()), // ApertureArea
        new Optional(new ParseDouble()), // NumberCellsSeries
        new Optional(new ParseDouble()), // NumberCellsParallel
        new Optional(), // Type
        new Optional(), // Family
        new Optional(), // Technology
        new Optional(), // P2Pref
        new Optional(), // PercentNonlin
        new Optional(), // a_ref
        new Optional(), // I_o_ref
        new Optional(), // I_L_ref
        new Optional(), // R_s_ref
        new Optional(), // R_sh_ref
        new Optional(), // delta
        new Optional(), // I_sc_adj
        new Optional(), // Time
        new Optional(), // Version
        new Optional(), // Mounting
        new Optional(), // LastUpdate
        new Optional() // CECListingDate
    };
    return processors;
}

And, likewise the CellProcessors for the PV Modules table.

Then finally,

@PostConstruct
private void loadData() throws IOException {

    ICsvBeanReader beanReader = null;
    try {
        Resource resource = new ClassPathResource(pvDataInvertersCSV);
        beanReader = new CsvBeanReader(
                new InputStreamReader(resource.getInputStream()),
                CsvPreference.STANDARD_PREFERENCE);

        final String[] header = beanReader.getHeader(true);
        final CellProcessor[] inverterProcessors = getInverterProcessors();

        Inverter inverter;
        while ((inverter = beanReader.read(Inverter.class, header, inverterProcessors)) != null) {
            invertersRepository.save(inverter);
        }

    }
    finally {
        if (beanReader != null) {
            beanReader.close();
        }
    }
    try {
        Resource resource = new ClassPathResource("PV_Module_List_Full_Data.csv");
        beanReader = new CsvBeanReader(
                new InputStreamReader(resource.getInputStream()),
                CsvPreference.STANDARD_PREFERENCE);

        final String[] header = beanReader.getHeader(true);
        final CellProcessor[] moduleProcessors = getModuleProcessors();

        Module module;
        while ((module = beanReader.read(Module.class, header, moduleProcessors)) != null) {
            modulesRepository.save(module);
        }

    }
    finally {
        if (beanReader != null) {
            beanReader.close();
        }
    }
}

The @PostConstruct annotation means this is called after the constructor is called.

Because the CSV files are in the resources directory, they're available as a ClassPathResource as shown here. It means the CSV cannot be read directly from the regular filesystem, because the files are stuffed inside the JAR file. Fortunately, Resource can provide an InputStream and we can pass an InputStreamReader to CsvBeanReader.

The CsvBeanReader class performs a lot of magic for us. In beanReader.read it automagically matches up columns in the CSV file with fields in the Model class. We haven't defined the Model classes yet (Inverter and Module). What's required is for the column names to match up with a setColumnName function in the Model class. As long as that association exists, beanReader.read will successfully insert column values into fields in each Model class instance. Then the save function writes the object to the database table.

So long as this issue is fresh in our mind, lets focus on the Model classes.

Model package and classes

Create a model package such as com.davidherron.pvmodulesinverters.model. In that package create a class named Module, and another named Inverter. As the names imply, each instance will hold data for a single PV Module or Solar Inverter.

It's possible to simply copy over the model package Swagger created for you. We will be modifying these packages a little.

The Inverter class:

/**
 * Inverter
 */
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2017-07-25T02:09:48.524Z")
public class Inverter   {

  @Id
  int id;

  @JsonProperty("manufacturerName")
  @JsonPropertyDescription("Manufacturer name")
  private String manufacturerName = null;

  @JsonProperty("modelNumber")
  @JsonPropertyDescription("Manufacturer-assigned model number")
  private String modelNumber = null;

  @JsonProperty("description")
  @JsonPropertyDescription("Manufacturer's description")
  private String description = null;

  @JsonProperty("powerRating")
  @JsonPropertyDescription("Power Rating, Continuous at Unity Power Factor (kW)")
  private Double powerRating = null;

  @JsonProperty("nominalVoltage")
  @JsonPropertyDescription("Nominal Voltage (Vac)")
  private String nominalVoltage = null;

  @JsonProperty("weightedEfficiency")
  @JsonPropertyDescription("Weighted Efficiency (%)")
  private Double weightedEfficiency = null;

  @JsonProperty("UL1741SA")
  @JsonPropertyDescription("UL 1741 Supplement A Certification*** (Certification Date)")
  private String ul1741SA = null;

  @JsonProperty("firmwareVersionTested")
  @JsonPropertyDescription("Firmware Version Tested")
  private String firmwareVersionTested = null;

  @JsonProperty("builtInMeter")
  @JsonPropertyDescription("")
  private String builtInMeter = null;

  @JsonProperty("microInverter")
  @JsonPropertyDescription("Built-In Meter")
  private String microInverter = null;

  @JsonProperty("notes")
  @JsonPropertyDescription("Manufacturer's notes")
  private String notes = null;

  @JsonProperty("nightTareLoss")
  @JsonPropertyDescription("Night Tare Loss (W)")
  private Double nightTareLoss = null;

  @JsonProperty("powerRatingContinuous40degC")
  @JsonPropertyDescription("Power Rating, Continuous, 40 deg C (kW)")
  private Double powerRatingContinuous40degC = null;

  @JsonProperty("nightTareLoss40degC")
  @JsonPropertyDescription("Night Tare Loss 40 deg C (W)")
  private Double nightTareLoss40degC = null;

  @JsonProperty("voltageMinDC")
  @JsonPropertyDescription("Voltage Minimum (Vdc)")
  private Double voltageMinDC = null;

  @JsonProperty("voltageNominalDC")
  @JsonPropertyDescription("Voltage Nominal (Vdc)")
  private Double voltageNominalDC = null;

  @JsonProperty("voltageMaxDC")
  @JsonPropertyDescription("Voltage Maximum (Vdc)")
  private Double voltageMaxDC = null;

  @JsonProperty("powerLevel10")
  @JsonPropertyDescription("Power Level 10% (kW)")
  private Double powerLevel10 = null;

  @JsonProperty("powerLevel20")
  @JsonPropertyDescription("Power Level 20% (kW)")
  private Double powerLevel20 = null;

  @JsonProperty("powerLevel30")
  @JsonPropertyDescription("Power Level 30% (kW)")
  private Double powerLevel30 = null;

  @JsonProperty("powerLevel50")
  @JsonPropertyDescription("Power Level 50% (kW)")
  private Double powerLevel50 = null;

  @JsonProperty("powerLevel75")
  @JsonPropertyDescription("Power Level 75% (kW)")
  private Double powerLevel75 = null;

  @JsonProperty("powerLevel100")
  @JsonPropertyDescription("Power Level 100% (kW)")
  private Double powerLevel100 = null;

  @JsonProperty("efficiencyVmin10")
  @JsonPropertyDescription("Efficiency @Vmin, 10% (%)")
  private Double efficiencyVmin10 = null;

  @JsonProperty("efficiencyVmin20")
  @JsonPropertyDescription("Efficiency @Vmin, 20% (%)")
  private Double efficiencyVmin20 = null;

  @JsonProperty("efficiencyVmin30")
  @JsonPropertyDescription("Efficiency @Vmin, 30% (%)")
  private Double efficiencyVmin30 = null;

  @JsonProperty("efficiencyVmin50")
  @JsonPropertyDescription("Efficiency @Vmin, 50% (%)")
  private Double efficiencyVmin50 = null;

  @JsonProperty("efficiencyVmin75")
  @JsonPropertyDescription("Efficiency @Vmin, 75% (%)")
  private Double efficiencyVmin75 = null;

  @JsonProperty("efficiencyVmin100")
  @JsonPropertyDescription("Efficiency @Vmin, 100% (%)")
  private Double efficiencyVmin100 = null;

  @JsonProperty("efficiencyVmin")
  @JsonPropertyDescription("Efficiency @Vmin")
  private Double efficiencyVmin = null;

  @JsonProperty("efficiencyVnom10")
  @JsonPropertyDescription("Efficiency @Vnom, 10% (%)")
  private Double efficiencyVnom10 = null;

  @JsonProperty("efficiencyVnom20")
  @JsonPropertyDescription("Efficiency @Vnom, 20% (%)")
  private Double efficiencyVnom20 = null;

  @JsonProperty("efficiencyVnom30")
  @JsonPropertyDescription("Efficiency @Vnom, 30% (%)")
  private Double efficiencyVnom30 = null;

  @JsonProperty("efficiencyVnom50")
  @JsonPropertyDescription("Efficiency @Vnom, 50% (%)")
  private Double efficiencyVnom50 = null;

  @JsonProperty("efficiencyVnom75")
  @JsonPropertyDescription("Efficiency @Vnom, 75% (%)")
  private Double efficiencyVnom75 = null;

  @JsonProperty("efficiencyVnom100")
  @JsonPropertyDescription("Efficiency @Vnom, 100% (%)")
  private Double efficiencyVnom100 = null;

  @JsonProperty("efficiencyVnom")
  @JsonPropertyDescription("Efficiency @Vnom")
  private Double efficiencyVnom = null;

  @JsonProperty("efficiencyVmax10")
  @JsonPropertyDescription("Efficiency @Vmax, 10% (%)")
  private Double efficiencyVmax10 = null;

  @JsonProperty("efficiencyVmax20")
  @JsonPropertyDescription("Efficiency @Vmax, 20% (%)")
  private Double efficiencyVmax20 = null;

  @JsonProperty("efficiencyVmax30")
  @JsonPropertyDescription("Efficiency @Vmax, 30% (%)")
  private Double efficiencyVmax30 = null;

  @JsonProperty("efficiencyVmax50")
  @JsonPropertyDescription("Efficiency @Vmax, 50% (%)")
  private Double efficiencyVmax50 = null;

  @JsonProperty("efficiencyVmax75")
  @JsonPropertyDescription("Efficiency @Vmax, 75% (%)")
  private Double efficiencyVmax75 = null;

  @JsonProperty("efficiencyVmax100")
  @JsonPropertyDescription("Efficiency @Vmax, 100% (%)")
  private Double efficiencyVmax100 = null;

  @JsonProperty("efficiencyVmax")
  @JsonPropertyDescription("Efficiency @Vmax")
  private Double efficiencyVmax = null;

  @JsonProperty("sortOrder")
  @JsonPropertyDescription("Sort order")
  private String sortOrder = null;

  @JsonProperty("cecListingDate")
  @JsonPropertyDescription("Listing date with California Energy Commission")
  private String cecListingDate = null;

  @JsonProperty("lastUpdate")
  @JsonPropertyDescription("Last update")
  private String lastUpdate = null;
}

Obviously this only defines the fields. We've added @JsonProperty annotations to help Swagger generate good quality documentation. The @JsonPropertyDescription annotation is added over what Swagger generated, and adds the field description.

Assuming you're using an IDE like Eclipse, you can now generate getter/setter/equals/hash/toString methods. In front of each get method it's useful to add something like this:

  /**
   * Efficiency @Vnom, 20% (%)
   * @return efficiencyVnom20
   **/
  @ApiModelProperty(value = "Efficiency @Vnom, 20% (%)")

This will generate good JavaDoc documents, and even more details to help Swagger generate good quality documentation.

Similarly the Module class is:

/**
 * Module
 */
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2017-07-25T02:09:48.524Z")
public class Module {

    @Id
    int id;

    @JsonProperty("manufacturer")
    @JsonPropertyDescription("Manufacturer name")
    private String manufacturer = null;

    @JsonProperty("modelNumber")
    @JsonPropertyDescription("Manufacturer-assigned model number")
    private String modelNumber = null;

    @JsonProperty("description")
    @JsonPropertyDescription("Manufacturer's description")
    private String description = null;

    @JsonProperty("bipv")
    @JsonPropertyDescription("Building Integrated Photovoltaic Module")
    private String BIPV = null;

    @JsonProperty("nameplatePmax")
    @JsonPropertyDescription("Maximum power at standard test conditions")
    private Double nameplatePmax = null;

    @JsonProperty("ptc")
    @JsonPropertyDescription("Power value at PV USA Test Conditions")
    private Double PTC = null;

    @JsonProperty("notes")
    @JsonPropertyDescription("Manufacturer's notes")
    private String notes = null;

    @JsonProperty("nameplateVpmax")
    @JsonPropertyDescription("Voltage at Maximum Power (Vpmax)")
    private Double nameplateVpmax = null;

    @JsonProperty("nameplateIpmax")
    @JsonPropertyDescription("Current at Maximum Power (Ip max)")
    private Double nameplateIpmax = null;

    @JsonProperty("nameplateVoc")
    @JsonPropertyDescription("Open Circuit Voltage (Voc)")
    private Double nameplateVoc = null;

    @JsonProperty("nameplateIsc")
    @JsonPropertyDescription("Short Circuit Current (Isc)")
    private Double nameplateIsc = null;

    @JsonProperty("averageNOCT")
    @JsonPropertyDescription("Nominal Operating Cell Temperature. The temperature reached by open circuited cells in a PV "
        + "module under the following conditions: 800 watts per square meter irradiance on cell surface, 20 "
        + "degrees Celsius air temperature, 1 meter per second wind velocity, and open back side mounting")
    private Double averageNOCT = null;

    @JsonProperty("betaVoc")
    @JsonPropertyDescription("Temperature Coefficient of Open Circuit Voltage, percent per degrees Celsius (β Voc)")
    private Double betaVoc = null;

    @JsonProperty("betaVpmax")
    @JsonPropertyDescription("Temperature Coefficient of Voltage at Maximum Power, percent per degrees Celsius (β Vp max)")
    private Double betaVpmax = null;

    @JsonProperty("alphaIsc")
    @JsonPropertyDescription("Temperature Coefficient of Short Circuit Current, percent per degrees Celsius (α Isc)")
    private Double alphaIsc = null;

    @JsonProperty("alphaIpmax")
    @JsonPropertyDescription("Temperature Coefficient of Current at Maximum Power, percent per degrees Celsius (α Ip max)")
    private Double alphaIpmax = null;

    @JsonProperty("gammaPmax")
    @JsonPropertyDescription("Temperature Coefficient of Maximum Power, percent per degrees Celsius (γ Pmax)")
    private Double gammaPmax = null;

    @JsonProperty("VPmaxlow")
    @JsonPropertyDescription("Voltage at Maximum Power and Low Irradiance (V Pmax, low)")
    private Double vpmaxlow = null;

    @JsonProperty("IPmaxlow")
    @JsonPropertyDescription("Current at Maximum Power and Low Irradiance (I Pmax, low)")
    private Double ipmaxlow = null;

    @JsonProperty("VPmaxNOCT")
    @JsonPropertyDescription("Voltage at Maximum Power and NOCT. (V Pmax, NOCT)")
    private Double vpmaxNOCT = null;

    @JsonProperty("IPmaxNOCT")
    @JsonPropertyDescription("Current at Maximum Power and NOCT (I Pmax, NOCT)")
    private Double ipmaxNOCT = null;

    @JsonProperty("shortSide")
    @JsonPropertyDescription("Size on short side, in Meters")
    private Double shortSide = null;

    @JsonProperty("longSide")
    @JsonPropertyDescription("Size on long side, in Meters")
    private Double longSide = null;

    @JsonProperty("geometricMultiplier")
    @JsonPropertyDescription("Geometric Multiplier")
    private Double geometricMultiplier = null;

    @JsonProperty("apertureArea")
    @JsonPropertyDescription("")
    private Double ApertureArea = null;

    @JsonProperty("numberCellsSeries")
    @JsonPropertyDescription("Number of Cells in Series")
    private Double NumberCellsSeries = null;

    @JsonProperty("numberCellsParallel")
    @JsonPropertyDescription("Number of Cells in Parallel")
    private Double NumberCellsParallel = null;

    @JsonProperty("type")
    @JsonPropertyDescription("Module type")
    private String type = null;

    @JsonProperty("family")
    @JsonPropertyDescription("Cell Technology family")
    private String family = null;

    @JsonProperty("technology")
    @JsonPropertyDescription("Cell Technology type")
    private String technology = null;

    @JsonProperty("p2pref")
    @JsonPropertyDescription("P2Pref")
    private String p2pref = null;

    @JsonProperty("percentNonlin")
    @JsonPropertyDescription("PercentNonlin")
    private String percentNonlin = null;

    @JsonProperty("a_ref")
    @JsonPropertyDescription("a_ref")
    private String a_ref = null;

    @JsonProperty("I_o_ref")
    @JsonPropertyDescription("I_o_ref")
    private String I_o_ref = null;

    @JsonProperty("I_L_ref")
    @JsonPropertyDescription("I_L_ref")
    private String I_L_ref = null;

    @JsonProperty("R_s_ref")
    @JsonPropertyDescription("R_s_ref")
    private String R_s_ref = null;

    @JsonProperty("R_sh_ref")
    @JsonPropertyDescription("R_sh_ref")
    private String R_sh_ref = null;

    @JsonProperty("delta")
    @JsonPropertyDescription("delta")
    private String delta = null;

    @JsonProperty("I_sc_adj")
    @JsonPropertyDescription("I_sc_adj")
    private String I_sc_adj = null;

    @JsonProperty("time")
    @JsonPropertyDescription("Time")
    private String time = null;

    @JsonProperty("version")
    @JsonPropertyDescription("Version")
    private String version = null;

    @JsonProperty("mounting")
    @JsonPropertyDescription("How the module is mounted")
    private String mounting = null;

    @JsonProperty("lastUpdate")
    @JsonPropertyDescription("Last update")
    private String lastUpdate = null;

    @JsonProperty("cecListingDate")
    @JsonPropertyDescription("Listing date with California Energy Commission")
    private String cecListingDate = null;
}

Controller class(es)

We could simply copy over the api package generated by Swagger. But, the generated package was overly complex. Let's do something better.

Create a controller package such as com.davidherron.pvmodulesinverters.controller and in that package create a class named Controller.


@RestController
public class Controller {

    @Autowired
    private ModulesService modulesService;

    @Autowired
    private InvertersService invertersService;

    @ApiOperation(value = "", nickname = "getModules", notes = "Query the PV Modules database", response = Module.class, responseContainer = "List", tags={  })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "OK", response = Module.class, responseContainer = "List") })
    @RequestMapping(method = RequestMethod.GET, value = "/modules")
    public List<Module> modules(
        @ApiParam(value = "Matches against manufacturer name") @RequestParam(value = "manufacturer", required = false) String manufacturer,
        @ApiParam(value = "Matches against manufacturer-assigned model number") @RequestParam(value = "model", required = false) String model,
        @ApiParam(value = "Minimum of allowable Pmax range") @RequestParam(value = "minPmax", required = false) Float minPmax,
        @ApiParam(value = "Maximum of allowable Pmax range") @RequestParam(value = "maxPmax", required = false) Float maxPmax,
        @ApiParam(value = "Minimum of allowable Vpmax range") @RequestParam(value = "minVpmax", required = false) Float minVpmax,
        @ApiParam(value = "Maximum of allowable Vpmax range") @RequestParam(value = "maxVpmax", required = false) Float maxVpmax,
        @ApiParam(value = "Minimum of allowable Voc range") @RequestParam(value = "minVoc", required = false) Float minVoc,
        @ApiParam(value = "Maximum of allowable Voc range") @RequestParam(value = "maxVoc", required = false) Float maxVoc,
        @ApiParam(value = "Minimum of allowable Isx range") @RequestParam(value = "minIsc", required = false) Float minIsc,
        @ApiParam(value = "Maximum of allowable Isc range") @RequestParam(value = "maxIsc", required = false) Float maxIsc,
        @ApiParam(value = "Matches against the panel family", allowableValues = "monocrystalline, polycrystalline, thinfilm, cigsthin, hitsithin") @RequestParam(value = "family", required = false) String family,
        @ApiParam(value = "Matches against the panel technology", allowableValues = "monocsi, multicsi, thinfilm") @RequestParam(value = "technology", required = false) String technology,
        @RequestHeader(value = "Accept", required = false) String accept) {

        return modulesService.searchForModules(manufacturer, model, minPmax, maxPmax,
            minVpmax, maxVpmax, minVoc, maxVoc, minIsc, maxIsc, family, technology);
    }

    @ApiOperation(value = "", nickname = "getInverters", notes = "Query the PV Inverters database", response = Inverter.class, responseContainer = "List", tags={  })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "OK", response = Inverter.class, responseContainer = "List") })
    @RequestMapping(value = "/inverters", method = RequestMethod.GET)
    public List<Inverter> getInverters(
        @ApiParam(value = "Matches against manufacturer name") @RequestParam(value = "manufacturer", required = false) String manufacturer,
        @ApiParam(value = "Matches against manufacturer-assigned model number") @RequestParam(value = "model", required = false) String model,
        @ApiParam(value = "Minimum power continuous rating") @RequestParam(value = "minpower", required = false) Float minpower,
        @ApiParam(value = "Maximum power continuous rating") @RequestParam(value = "maxpower", required = false) Float maxpower,
        @ApiParam(value = "Minimum nominal voltage") @RequestParam(value = "minvoltage", required = false) Float minvoltage,
        @ApiParam(value = "Maximum nominal voltage") @RequestParam(value = "maxvoltage", required = false) Float maxvoltage,
        @ApiParam(value = "Has a built-in meter", allowableValues = "true, false") @RequestParam(value = "hasmeter", required = false) String hasmeter,
        @ApiParam(value = "Is a microinverter system", allowableValues = "true, false") @RequestParam(value = "microinverter", required = false) String microinverter,
        @RequestHeader(value = "Accept", required = false) String accept) throws Exception {

        List<Inverter> inverters = invertersService.searchForInverters(
            manufacturer, model, minpower, maxpower,
            minvoltage, maxvoltage, hasmeter, microinverter);

        return inverters;
    }

    @ExceptionHandler
    void handleIllegalArgumentException(
                      IllegalArgumentException e,
                      HttpServletResponse response) throws IOException {

        response.sendError(HttpStatus.BAD_REQUEST.value());

    }
}

Compare between the Swagger-generated api module and this. It's got all the same annotations, and it's a lot simpler.

It makes calls over to the Service classes that we haven't created yet.

Service classes

In your service package, such as com.davidherron.pvmodulesinverters.service, we need to create a pair of Service's for Inverters and Modules.

Create a class named InvertersService using this:

@Service
public class InvertersService {

    @Autowired
    private InvertersRepository invertersRespository;

    public List<Inverter> searchForInverters(
            String manufacturer, String model, Float minpower,
            Float maxpower, Float minvoltage, Float maxvoltage,
            String hasmeter, String microinverter) {

        return invertersRespository.searchForInverters(
            manufacturer, model, minpower, maxpower, minvoltage, maxvoltage,
            hasmeter, microinverter);
    }

}

This passes the request from the Controller over to the corresponding Repository class. Of course we haven't created that class, yet.

Likewise, create a ModulesService class containing:

@Service
public class ModulesService {

    @Autowired
    private ModulesRepository modulesRepository;

    public List<Module> searchForModules(
            String manufacturer, String model, Float minPmax, Float maxPmax,
            Float minVpmax, Float maxVpmax, Float minVoc, Float maxVoc, Float minIsc,
            Float maxIsc, String family, String technology) {

        return modulesRepository.searchForModules(
            manufacturer, model, minPmax, maxPmax, minVpmax, maxVpmax, minVoc,
            maxVoc, minIsc, maxIsc, family, technology);
    }

}

Repository classes

The Repository classes interface with the data repository -- a.k.a. database. This is where we use JDBC calls to read from the database, and save data to the database. Since this service does not implement the full CRUD paradigm, we do not need the full CRUD functions.

Create a repository package, such as com.davidherron.pvmodulesinverters.repository.

InvertersRepository

Create a class named InvertersRepository containing:

@Repository
public class InvertersRepository {

    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    private SimpleJdbcInsert insertInverter;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
        this.insertInverter = new SimpleJdbcInsert(dataSource)
                .withTableName("inverters")
                .usingGeneratedKeyColumns("id");
    }
}

We've marked this with @Repository so that Spring knows what it is. It's widely recommended to use the CrudRepository class to simplify the implementation which follows. The limitation is that we have a lot of optional parameters, which doesn't play well with CrudRepository (so far as I know).

The dataSource is what was defined in application.yaml. The namedParameterJdbcTemplate makes database queries easier to perform, and the insertInverter makes saving data to the database very simple. The usingGeneratedKeyColumns call on the latter is where the id field is set. This value does not come from the CSV file, instead the schema defined this as an AUTO_INCREMENT field. We may find it useful to have a unique numerical ID for each of the objects, and that's what we get for free.

@SuppressWarnings({ "unchecked", "rawtypes" })
public List<Inverter> searchForInverters(
        String manufacturer, String model, Float minpower,
        Float maxpower, Float minvoltage, Float maxvoltage,
        String hasmeter, String microinverter) {

    String sql = "SELECT * FROM inverters ";
    String where = "";
    MapSqlParameterSource namedParameters = new MapSqlParameterSource();

    if (manufacturer != null) {
        where += "ManufacturerName = :manufacturer";
        namedParameters.addValue("manufacturer", manufacturer);
    }
    if (model != null) {
        if (where != "") where += " AND ";
        where += "ModelNumber = :model";
        namedParameters.addValue("model", model);
    }
    if (minpower != null) {
        if (where != "") where += " AND ";
        where += "PowerRatingContinuous >= :minpower";
        namedParameters.addValue("minpower", minpower);
    }
    if (maxpower != null) {
        if (where != "") where += " AND ";
        where += "PowerRatingContinuous <= :maxpower";
        namedParameters.addValue("maxpower", maxpower);
    }
    if (minvoltage != null) {
        if (where != "") where += " AND ";
        where += "NominalVoltage >= :minvoltage";
        namedParameters.addValue("minvoltage", minvoltage);
    }
    if (maxvoltage != null) {
        if (where != "") where += " AND ";
        where += "NominalVoltage <= :maxvoltage";
        namedParameters.addValue("maxvoltage", maxvoltage);
    }
    if (hasmeter != null) {
        if (where != "") where += " AND ";
        where += "BuiltInMeter = :hasmeter";
        namedParameters.addValue("hasmeter", hasmeter);
    }
    if (microinverter != null) {
        if (where != "") where += " AND ";
        where += "Microinverter = :microinverter";
        namedParameters.addValue("microinverter", microinverter);
    }

    if (where != "") where = " WHERE "+ where;
    sql = sql + where + " ORDER BY sortOrder";

    return this.namedParameterJdbcTemplate.query(sql, namedParameters, new RowMapper() {

        @Override
        public Object mapRow(ResultSet arg0, int arg1) throws SQLException {
            Inverter inverter = new Inverter();
            inverter
                .manufacturerName(arg0.getString("ManufacturerName"))
                .modelNumber(arg0.getString("ModelNumber"))
                .description(arg0.getString("Description"))
                .powerRating(arg0.getDouble("PowerRatingContinuous"))
                .nominalVoltage(arg0.getString("NominalVoltage"))
                .weightedEfficiency(arg0.getDouble("WeightedEfficiency"))
                .ul1741SA(arg0.getString("UL1741SupplementACertification"))
                .firmwareVersionTested(arg0.getString("FirmwareVersionTested"))
                .builtInMeter(arg0.getString("BuiltInMeter"))
                .microInverter(arg0.getString("Microinverter"))
                .notes(arg0.getString("Notes"))
                .nightTareLoss(arg0.getDouble("NightTareLoss"))
                .powerRatingContinuous40degC(arg0.getDouble("PowerRatingContinuous40degC"))
                .nightTareLoss40degC(arg0.getDouble("NightTareLoss40degC"))
                .voltageMinDC(arg0.getDouble("VoltageMinimum"))
                .voltageNominalDC(arg0.getDouble("VoltageNominal"))
                .voltageMaxDC(arg0.getDouble("VoltageMaximum"))
                .powerLevel10(arg0.getDouble("PowerLevel10"))
                .powerLevel20(arg0.getDouble("PowerLevel20"))
                .powerLevel30(arg0.getDouble("PowerLevel30"))
                .powerLevel50(arg0.getDouble("PowerLevel50"))
                .powerLevel75(arg0.getDouble("PowerLevel75"))
                .powerLevel100(arg0.getDouble("PowerLevel100"))
                .efficiencyVmin10(arg0.getDouble("EfficiencyVmin10"))
                .efficiencyVmin20(arg0.getDouble("EfficiencyVmin20"))
                .efficiencyVmin30(arg0.getDouble("EfficiencyVmin30"))
                .efficiencyVmin50(arg0.getDouble("EfficiencyVmin50"))
                .efficiencyVmin75(arg0.getDouble("EfficiencyVmin75"))
                .efficiencyVmin100(arg0.getDouble("EfficiencyVmin100"))
                .efficiencyVnom10(arg0.getDouble("EfficiencyVnom10"))
                .efficiencyVnom20(arg0.getDouble("EfficiencyVnom20"))
                .efficiencyVnom30(arg0.getDouble("EfficiencyVnom30"))
                .efficiencyVnom50(arg0.getDouble("EfficiencyVnom50"))
                .efficiencyVnom75(arg0.getDouble("EfficiencyVnom75"))
                .efficiencyVnom100(arg0.getDouble("EfficiencyVnom100"))
                .efficiencyVmax10(arg0.getDouble("EfficiencyVmax10"))
                .efficiencyVmax20(arg0.getDouble("EfficiencyVmax20"))
                .efficiencyVmax30(arg0.getDouble("EfficiencyVmax30"))
                .efficiencyVmax50(arg0.getDouble("EfficiencyVmax50"))
                .efficiencyVmax75(arg0.getDouble("EfficiencyVmax75"))
                .efficiencyVmax100(arg0.getDouble("EfficiencyVmax100"))
                .sortOrder(arg0.getString("SortOrder"))
                .cecListingDate(arg0.getString("CECListingDate"))
                .lastUpdate(arg0.getString("LastUpdate"));
            return inverter;
        }
    });
}

This is where we handle all the optional parameters in the Controller class. Our API methods, with no parameters, will return every last PV Module or Inverter in the database. As we add parameters to the query, that filters the result set making it more manageable.

We handle the optional parameters by constructing our SQL SELECT statement dynamically. For any field in the query, we add a clause to the SELECT statement. Finally, since there is a supplied sortOrder field, we use that field to sort the result set.

The query requires a RowMapper instance. For each row in the result set, the mapRow function is called. We are supposed to create an Inverter instance, fill in its fields, and return that to the RowMapper. There doesn't seem to be a method to do this automagically, so we do it by hand instead.

The methodology used here is to implement a fluid API to the Model class. Rather than a bunch of setXYZZY function calls, we use a chained pseudo-setter function. When we generated the getters and setters earlier, there was not a suitable chainable setter function added.

We have to go back to the Inverter class, and make sure that each getter/setter follows this pattern:

  public Inverter manufacturerName(String manufacturerName) {
    this.manufacturerName = manufacturerName;
    return this;
  }

  /**
   * Manufacturer name
   * @return manufacturer
   **/
  @ApiModelProperty(value = "Manufacturer name")
  public String getManufacturerName() {
    return manufacturerName;
  }

  public void setManufacturerName(String manufacturerName) {
    this.manufacturerName = manufacturerName;
  }

The 1st of these functions is what we need to add, since the 2nd and 3rd will have been generated when we generated getters/setters. This first method is to be named to exactly correspond to the field name. It takes the value to assign, and then it returns the object instance, so that back in the Repository function we can chain each setter call as shown.

public void save(Inverter inverter) {
    SqlParameterSource parameters = new BeanPropertySqlParameterSource(inverter);
    insertInverter.execute(parameters);
}

Finally we need the save function. Spring is able to do a lot to help us here, since BeanPropertySqlParameterSource sets everything up so that insertInverter.execute magically maps the object fields to the database row fields.

ModulesRepository

Likewise, create a class named ModulesRepository containing:

@Repository
public class ModulesRepository {

    private static final Logger logger = LoggerFactory.getLogger(ModulesRepository.class);
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    private SimpleJdbcInsert insertModule;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
        this.insertModule = new SimpleJdbcInsert(dataSource)
            .withTableName("modules")
                .usingGeneratedKeyColumns("id");
    }

    @SuppressWarnings("unchecked")
    public List<Module> searchForModules(
            String manufacturer, String modelNumber, Float minPmax, Float maxPmax,
            Float minVpmax, Float maxVpmax, Float minVoc, Float maxVoc, Float minIsc,
            Float maxIsc, String family, String technology) {

        String sql = "SELECT * FROM modules ";
        String where = "";
        MapSqlParameterSource namedParameters = new MapSqlParameterSource();

        if (manufacturer != null) {
            where += "Manufacturer = :manufacturer";
            namedParameters.addValue("manufacturer", manufacturer);
        }
        if (modelNumber != null) {
            if (where != "") where += " AND ";
            where += "ModelNumber = :modelNumber";
            namedParameters.addValue("modelNumber", modelNumber);
        }
        if (minPmax != null) {
            if (where != "") where += " AND ";
            where += "NameplatePmax >= :minPmax";
            namedParameters.addValue("minPmax", minPmax);
        }
        if (maxPmax != null) {
            if (where != "") where += " AND ";
            where += "NameplatePmax <= :maxPmax";
            namedParameters.addValue("maxPmax", maxPmax);
        }
        if (minVpmax != null) {
            if (where != "") where += " AND ";
            where += "NameplateVpmax >= :minVpmax";
            namedParameters.addValue("minVpmax", minVpmax);
        }
        if (maxVpmax != null) {
            if (where != "") where += " AND ";
            where += "NameplateVpmax <= :maxVpmax";
            namedParameters.addValue("maxVpmax", maxVpmax);
        }
        if (minVoc != null) {
            if (where != "") where += " AND ";
            where += "NameplateVoc >= :minVoc";
            namedParameters.addValue("minVoc", minVoc);
        }
        if (maxVoc != null) {
            if (where != "") where += " AND ";
            where += "NameplateVoc <= :maxVoc";
            namedParameters.addValue("maxVoc", maxVoc);
        }
        if (minIsc != null) {
            if (where != "") where += " AND ";
            where += "NameplateIsc >= :minIsc";
            namedParameters.addValue("minIsc", minIsc);
        }
        if (maxIsc != null) {
            if (where != "") where += " AND ";
            where += "NameplateIsc <= :maxIsc";
            namedParameters.addValue("maxIsc", maxIsc);
        }
        if (family != null) {
            if (where != "") where += " AND ";
            where += "Family = :family";
            namedParameters.addValue("family", family);
        }
        if (technology != null) {
            if (where != "") where += " AND ";
            where += "Technology = :technology";
            namedParameters.addValue("technology", technology);
        }

        if (where != "") where = " WHERE "+ where;
        sql = sql + where;

        return (List<Module>) this.namedParameterJdbcTemplate.query(sql, namedParameters, new RowMapper<Module>() {

            @Override
            public Module mapRow(ResultSet arg0, int arg1) throws SQLException {
                Module module = new Module();
                module
                    .manufacturer(arg0.getString("Manufacturer"))
                    .modelNumber(arg0.getString("ModelNumber"))
                    .description(arg0.getString("Description"))
                    .BIPV(arg0.getString("BIPV"))
                    .nameplatePmax(arg0.getDouble("NameplatePmax"))
                    .PTC(arg0.getDouble("PTC"))
                    .notes(arg0.getString("Notes"))
                    .nameplateVpmax(arg0.getDouble("NameplateVpmax"))
                    .nameplateIpmax(arg0.getDouble("NameplateIpmax"))
                    .nameplateVoc(arg0.getDouble("NameplateVoc"))
                    .nameplateIsc(arg0.getDouble("NameplateIsc"))
                    .averageNOCT(arg0.getDouble("AverageNOCT"))
                    .betaVoc(arg0.getDouble("betaVoc"))
                    .betaVpmax(arg0.getDouble("betaVpmax"))
                    .alphaIsc(arg0.getDouble("alphaIsc"))
                    .alphaIpmax(arg0.getDouble("alphaIpmax"))
                    .gammaPmax(arg0.getDouble("gammaPmax"))
                    .vpmaxlow(arg0.getDouble("VPmaxlow"))
                    .ipmaxlow(arg0.getDouble("IPmaxlow"))
                    .vpmaxNOCT(arg0.getDouble("VPmaxNOCT"))
                    .ipmaxNOCT(arg0.getDouble("IPmaxNOCT"))
                    .shortSide(arg0.getDouble("ShortSide"))
                    .longSide(arg0.getDouble("LongSide"))
                    .geometricMultiplier(arg0.getDouble("GeometricMultiplier"))
                    .apertureArea(arg0.getDouble("ApertureArea"))
                    .numberCellsSeries(arg0.getDouble("NumberCellsSeries"))
                    .numberCellsParallel(arg0.getDouble("NumberCellsParallel"))
                    .type(arg0.getString("Type"))
                    .family(arg0.getString("Family"))
                    .technology(arg0.getString("Technology"))
                    .p2pref(arg0.getString("P2Pref"))
                    .percentNonlin(arg0.getString("PercentNonlin"))
                    .a_ref(arg0.getString("a_ref"))
                    .I_o_ref(arg0.getString("I_o_ref"))
                    .I_L_ref(arg0.getString("I_L_ref"))
                    .R_s_ref(arg0.getString("R_s_ref"))
                    .R_sh_ref(arg0.getString("R_sh_ref"))
                    .delta(arg0.getString("delta"))
                    .I_sc_adj(arg0.getString("I_sc_adj"))
                    .time(arg0.getString("Time"))
                    .version(arg0.getString("Version"))
                    .mounting(arg0.getString("Mounting"))
                    .lastUpdate(arg0.getString("LastUpdate"))
                    .cecListingDate(arg0.getString("CECListingDate"));
                return module;
            }

        });
    }

    public void save(Module module) {
        SqlParameterSource parameters = new BeanPropertySqlParameterSource(module);
        insertModule.execute(parameters);
    }
}

This is the very much the same as for InvertersRepository.

Swagger Configuration

One of our goals is configuring Swagger so we can browse the interactive documentation. What that means is the user interface similar to what's seen at the (petstore.swagger.io) http://petstore.swagger.io/ demo site. The methods, parameters, and results are all easily browsable in an interactive user interface. Not only that, you can run queries against the API from the documentation.

Another goal is to export the OpenAPI specification corresponding to the code.

The dependencies added to the POM brought down the code required to do this. To get it functioning we need to implement one class so that Swagger knows what to do.

Create a package named config, such as com.davidherron.pvmodulesinverters.config. In that package create a class named SwaggerConfig. It appears the class must have that name, because otherwise the Swagger tools failed to start.

@Configuration
@EnableSwagger2
@EnableAutoConfiguration
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
               .select()
               .apis(RequestHandlerSelectors.any())
               .paths(PathSelectors.regex("/modules.*|/inverters.*"))     
               // .PathSelectors.any() // for all
               .build().apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        @SuppressWarnings("deprecation")
        ApiInfo apiInfo = new ApiInfo(
                "PV Modules and Inverters",
                "Microservice for querying a Module and Inverter database from GoSolarCalifornia",
                "1.0",
                "https://... URL for the Terms and Services",
                "David Herron <david@davidherron.com>",
                "a License of API",
                "https://... URL for the license");
        return apiInfo;
    }
}

The api function declares the set of REST endpoints we want to have documented. The apiInfo function returns some information about the API we've implemented.

Running the application

We now have a complete application. Requests arrive in the Controller, which passes the request to either ModulesService or InvertersService, which then passes the request to either InvertersRepository or ModulesRepository, which then does a JDBC query to retrieve data instantiating either a Module or Inverter object.

While you can easily build and run the application inside an IDE like Eclipse, let's see how to run it on its own. With the command mvn clean package, we instruct Maven to do some cleaning and to then compile and package the application into a JAR file. Running the application is then this simple:

$ java -jar target/JAR-FILE-NAME.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.5.RELEASE)

2017-07-31 15:29:33.094  INFO 2951 --- [           main] c.d.p.Example1Application                : Starting Example1Application v0.0.1-SNAPSHOT on MacBook-Pro-4 with PID 2951 (/Users/david/path/to/projectDir/target/JAR-FILE-NAME.jar started by david in /Users/david/path/to/projectDir)
2017-07-31 15:29:33.101  INFO 2951 --- [           main] c.d.p.Example1Application                : No active profile set, falling back to default profiles: default
2017-07-31 15:29:33.229  INFO 2951 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4dcbadb4: startup date [Mon Jul 31 15:29:33 PDT 2017]; root of context hierarchy

The output actually goes on for a long time, but you get the idea. The JAR file in the target directory is a complete runnable JAR executable with java -jar. That JAR file even contains a Tomcat instance pre-configured to run the Spring Boot application.

By default the port used for the service is (localhost) http://localhost:8080 as implied by the message: Tomcat started on port(s): 8080 (http)

During startup, among the messages printed is information about the mapped URL endpoints. Such as:

2017-07-31 15:39:20.848  INFO 3028 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/modules],methods=[GET],produces=[application/json]}" onto public java.util.List<com.davidherron.pvmodulesinverters.model.Module> com.davidherron.pvmodulesinverters.controller.Controller.modules(java.lang.String,java.lang.String,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.String,java.lang.String,java.lang.String)
2017-07-31 15:39:20.851  INFO 3028 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/inverters],methods=[GET],produces=[application/json]}" onto public java.util.List<com.davidherron.pvmodulesinverters.model.Inverter> com.davidherron.pvmodulesinverters.controller.Controller.getInverters(java.lang.String,java.lang.String,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.Float,java.lang.String,java.lang.String,java.lang.String) throws java.lang.Exception

This describes our two service URL's. You can go to, say, (localhost) http://localhost:8080/inverters?manufacturerName=ABB to get a list of ABB inverters in the database

At (localhost) http://localhost:8080/v2/api-docs you'll find the JSON version of the OpenAPI spec.

By default visiting (localhost) http://localhost:8080 redirects to the HAL Browser, at http://localhost:8080/browser/index.html#/

Unfortunately Swagger doesn't export the data necessary for the HAL Browser to properly browse the API. But this is one of the tools available to make REST queries and view the results. Go ahead and enter /inverters?manufacturerName=ABB into the query box, click the Go button, and you'll be given a JSON response showing the ABB inverters.

What IS available from Swagger is at: (localhost) http://localhost:8080/swagger-ui.html

This is the same user interface as for the (petstore.swagger.io) https://petstore.swagger.io example, but it shows OUR API.

Click on one of the two methods and you'll see something like this open up.

With this UI, you can browse the methods, the parameters, the response types, and even make requests of the API.

This isn't the sort of documentation you print into a book. Instead this is interactive documentation you can play with.

Generating an OpenAPI YAML file

We started at the top with an OpenAPI YAML file describing the API. We then ignored the source code generated from that file. We said we'd be able to generate a YAML file from the code, and then cross-verify the implementation was as specified.

At a command line run: curl -f http://localhost:8080/v2/api-docs

This will dump a bunch of JSON on the screen, and since it's unformatted you'll have a hard time making sense of it. But, you can copy the whole mess and go to (editor.swagger.io) http://editor.swagger.io/

First select File / Clear Editor to put the editor into a blank slate. You then paste the JSON into the editor pane, and a dialog pops up asking if you want that converted to YAML. Et voila, it's converted to YAML ...

The error message shown here is because of a bad URL announced for the software license.

« Booting a Raspberry Pi from a regular disk, avoiding the flaky SD card Organic Transit's ELF, solar powered car-bike for driver + 2 kids, equals 1800mpg »
2016 Election Acer C720 Ad block AkashaCMS Amiga Android Anti-Fascism Apple Apple Hardware History Apple iPhone Hardware April 1st Arduino ARM Compilation Astronomy Asynchronous Programming Authoritarianism Automated Social Posting Bells Law Big Brother Black Holes Blade Runner Blogger Blogging Books Botnet Botnets Cassette Tapes Cellphones Christopher Eccleston Chrome Chrome Apps Chromebook Chromebooks Chromebox ChromeOS CIA CitiCards Civil Liberties Clinton Cluster Computing Command Line Tools Computer Hardware Computer Repair Computers Cross Compilation Crouton Curiosity Rover Cyber Security Cybermen Daleks Darth Vader Data backup Data Storage Database Database Backup Databases David Tenant DDoS Botnet Detect Adblocker Digital Photography DIY DIY Repair Docker Doctor Who Doctor Who Paradox Drobo Drupal Drupal Themes DVD Early Computers Election Hacks Electric Bicycles Electric Vehicles Electron Emdebian Enterprise Node ESP8266 Ethical Curation Eurovision Event Driven Asynchronous Express Facebook Fake News File transfer without iTunes FireFly Fraud Freedom of Speech Gallifrey git Gitlab GMAIL Google Google Chrome Google Gnome Google+ Government Spying Great Britain Home Automation HTTPS I2C Protocol Image Analysis Image Conversion Image Processing ImageMagick InfluxDB Internet Internet Advertising Internet Law Internet of Things Internet Policy Internet Privacy iOS Devices iPad iPhone iPhone hacking Iron Man Iternet of Things iTunes Java JavaScript JavaScript Injection JDBC John Simms Joyent Lets Encrypt LibreOffice Linux Linux Hints Linux Single Board Computers Logging Mac OS Mac OS X Make Money Online MariaDB Mars Matt Lucas MEADS Anti-Missile Mercurial Michele Gomez Military Hardware Minification Minimized CSS Minimized HTML Minimized JavaScript Missy Mobile Applications MODBUS Mondas Monty Python MQTT Music Player Music Streaming MySQL NanoPi Nardole NASA Net Neutrality Node Web Development Node.js Node.js Database Node.js Testing Node.JS Web Development Node.x North Korea Online advertising Online Fraud Open Media Vault Open Source Governance Open Source Software OpenAPI OpenVPN Personal Flight Peter Capaldi Photography PHP Plex Media Server Political Protest Postal Service Power Control Privacy Production use Public Violence Raspberry Pi Raspberry Pi 3 Raspberry Pi Zero Recycling Remote Desktop Republicans Retro-Technology Reviews Right to Repair River Song Rocket Ships RSS News Readers rsync Russia Russia Troll Factory Scheme Science Fiction Season 1 Season 10 Season 11 Security Security Cameras Server-side JavaScript Shell Scripts Silence Simsimi Skype Social Media Warfare Social Networks Software Development Space Flight Space Ship Reuse Space Ships SpaceX Spring Spring Boot SQLite3 SSD Drives SSD upgrade SSH SSH Key SSL Swagger Synchronizing Files Telescopes Terrorism The Cybermen The Daleks The Master Time-Series Database Torchwood Total Information Awareness Trump Trump Administration Ubuntu UDOO Virtual Private Networks VirtualBox VLC VNC VOIP Web Applications Web Developer Resources Web Development Web Development Tools Weeping Angels WhatsApp Wordpress YouTube