If you’re using YAML, then you can also use profile documents inside the YAML file. Let’s take a closer look using the CommandLineRunner from last week’s post Externalized Configuration in Spring
@SpringBootApplication
public class Application implements CommandLineRunner {
@Value("${myApp.name}")
private String name;
@Value("${myApp.primary}")
private String primary = "foo";
@Value("${myApp.secondary}")
private String secondary = "foo";
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) throws Exception {
final SpringApplication app = new SpringApplicationBuilder(Application.class)
.properties("spring.config.name=myApp")
.build();
app.run();
}
public void run(String ... strings)
throws Exception
{
log.warn("name\t {}", name);
log.warn("primary\t {}", primary);
log.warn("secondary\t {}", secondary);
}
}
A YAML file is actually a sequence of documents separated by --- lines, and Spring parses each document separately to a flattened map. If a YAML document contains a spring.profiles key, then that value controls whether that document is included in the final merge.
Let’s take a look at an example. Here is the file packaged in my jar:
myApp:
name: Package Default
primary: Package Default
secondary: Package Default
---
spring.profiles: one
myApp:
name: Package 1
---
spring.profiles: two
myApp:
name: Package 22
primary: Package 22
---
spring.profiles: three
myApp:
primary: Package 333
secondary: Package 333
With no active profile, we get the values from the first document.
$ java -jar target/application-config-0.1.0.jar
WARN [main] name Package Default
WARN [main] primary Package Default
WARN [main] secondary Package Default
Set one as the active profile, to merge the first document with the document containing spring.profiles: one:
$ (export SPRING_PROFILES_ACTIVE=one ; java -jar target/application-config-0.1.0.jar)
WARN [main] name Package 1
WARN [main] primary Package Default
WARN [main] secondary Package Default
Set two,three as the active profiles, to merge the first document with the documents containing spring.profiles: two, spring.profiles: three:
$ (export SPRING_PROFILES_ACTIVE=two,three ; java -jar target/application-config-0.1.0.jar)
WARN [main] name Package 22
WARN [main] primary Package 333
WARN [main] secondary Package 333
And finally, add a local myApp.yml that sets the active profile. The merge order is:
$ cat myApp.yml
spring.profiles.active: one
myApp:
name: Local
secondary: Local
$ java -jar target/application-config-0.1.0.jar
WARN [main] name Package 1
WARN [main] primary Package Default
WARN [main] secondary Local
]]>But if you’re deploying to an application server, then there is a little tweak that you’ll need to keep in your back pocket - changing the spring.config.name from application to something else in code. This will let you add a centralized configuration directory to your application server classpath for all your deployed web applications, where setting spring.config.name by environment variable, command line options or JNDI don’t work).
Personally, I like using the org.springframework.boot.builder.SpringApplicationBuilder to change the spring.config.name to myApp:
app = new SpringApplicationBuilder(Application.class)
.properties("spring.config.name=myApp")
.build()
.run();
But, it can can also be done with the standard org.springframework.boot.SpringConfiguration.
app = new SpringApplication(Application.class);
Properties props = new Properties();
props.setProperty("spring.config.name","myApp");
app.setDefaultProperties(props);
app.run();
It is also possible to externalize the configuration with org.springframework.context.annotation.PropertySource, but this forgoes some application properties magic:
CBS has given me back 22 hours a year. I’d rather spend those 22 hours with the Machine.
]]>This year I was guardedly pessimistic. All season long I had been saying that the Broncos didn’t need Peyton Manning at the height of his powers - they just needed Peyton to play at a replacement level and the defense could do the rest. Unfortunately, Peyton wasn’t able to play at a replacement level. The result was a game in which Denver never trailed, but kept me on the edge of my seat most of the game.
After obsessively reviewing the post game analysis, I subscribe to the theory that the Carolina coaching staff let their team down while the Denver staff understood how to the Broncos could/would win. With the Broncos unlikely to put together a long TD drive, it was imperative that the Panthers manage field position and not give the Broncos a short field. Instead the Panthers gave up a defensive touchdown on their second position and the Broncos were on their way.
]]>I really think Carolina’s coaches didn’t appreciate the kind of game they were in, and it led to handing the Broncos their best chance to win the game. If Rivera and Co. had approached it with the sort of thinking that goes along with trying to win a game 16-13 or 13-9, all sorts of things might have been different. In particular, if Carolina approaches the game with the 1st principal being giving the Broncos zero chance to get a turnover on a very short field, the game might turn out different.
I was prepared for a defensive game when both both semi-final games went to penalty kicks after playing to 0-0 draws. But Jordan Morris scored for Stanford just 87 seconds into the final and the Cardinal never looked back.
]]>I thought that my front page ended a bit abruptly after removing the ability to page through the entire weblog. The tag cloud provides some guidance in exploring my back catalog and is enabled by the latest update to my hexo-theme-landscape work branch.
It’s feels odd, not having a link to my Java Tutorials. Getting my tutorial page on DMOZ was a big step 12 years ago. It was a strong effort back then, but I let it wither on the vine. Now it is time to move on.
]]>I have almost 900 total posts. That means I invalidate about 90 URLs with every post (10 posts per page). Or about 10 posts a month, unless I’m willing to pay. I don’t think anyone really wants to page through my entire weblog. And I already updated my archive (2015), category (technology), and tag (java) pages to present a single page of titles last year. So I’ve updated the hexo landscape theme to only generate the first index page.
My fork of hexo-theme-landscape is on github. Take the First Step uses the work branch. I cherry-pick commits from work to master so that I can generate clean pull requests from master.
]]>It turns out that I was making a mountain out of a mole hill. If you put the feature file in the wrong place, then cucumber will tell you where it was looking.
1 | $ mvn clean test -q -Dskip=client,browser |
So here’s the CucumberTest implementation of AbstractTestNGCucumberTests that does the work.
1 | package com.ideoplex.tutorial; |
Here’s the feature file, placed in src/test/resources/com/ideoplex/tutorial to match the package path of CucumberTest.
1 | Feature: User Management |
And the updates to the pom.xml to include the cucumber dependencies.
1 | <version>${jersey.client.version}</version> |
This version of Jersey, Gson and DataTables is on my github work branch. Normally I link to the master branch, but this feature is still half baked.
01 Nov Jersey Client with Gson
]]>It turns out that I was making a mountain out of a mole hill. If you put the feature file in the wrong place, then cucumber will tell you where it was looking.
1 | $ mvn clean test -q -Dskip=client,browser |
First, we need a UserMap deserializer to mirror the existing serializer. The existing serializer sends the UserMap as an object whose value is an Array of rows (this simplifies interaction with the DataTables front end). We deserialize the UserMap by iterating through the array, turning each row into a User object, and adding each User object to the Map.
1 | ... |
Next, we need to register the new UserMapUnmarshall class for use by the GsonReader class - just as we registered the UserMapMarshall class for the GsonWriter class.
1 | diff --git a/src/main/java/com/ideoplex/tutorial/GsonReader.java b/src/main/java/com/ideoplex/tutorial/GsonReader.java |
Let’s complete our suite of server services by adding a method to get a single user and a method to delete a user. The new feature in this code is the use of the PathParam annotation to extract values from the request path. And the remainder of the new code builds on features that we’ve used before.
1 | diff --git a/src/main/java/com/ideoplex/tutorial/MyResource.java b/src/main/java/com/ideoplex/tutorial/MyResource.java |
Now we can add a set of jersey client unit tests to exercise our server methods. I’ve split the code into multiple blocks for for clarity. The first block of code registers the GsonReader and GsonWriter classes for use by the client (explicit registration is not needed on the server because the automated server scan searches for the annotated classes).
1 | Client client; |
Execute a GET with an email path parameter:
1 | System.out.println("Verify that " + email + " is not available yet"); |
Execute a POST, submitting a User object and parsing the User object from the response.
1 | System.out.println("Verify that user/post on " + email + " succeeds"); |
Execute a GET, parsing the UserMap object from the response.
1 | System.out.println("Verify that " + email + " is in the userMap"); |
Execute a DELETE, parsing the User object from the response.
1 | System.out.println("Verify that delete on " + email + " succeeds"); |
And finally, a small change to the BrowserTest test annotation to insure out new client tests run before the selenium tests.
1 | diff --git a/src/test/java/com/ideoplex/tutorial/BrowserTest.java b/src/test/java/com/ideoplex/tutorial/BrowserTest.java |
The full source for this version of Jersey, Gson and Datatables is on github. BTW, I’m using jersey-client 1.18 for expediency, I’ll update this for 2.x in the near future.
31 Oct
jersey-gson Content-Type Woes
03 Nov Cucumber-java and TestNG
First, we need a UserMap deserializer to mirror the existing serializer. The existing serializer sends the UserMap as an object whose value is an Array of rows (this simplifies interaction with the DataTables front end). We deserialize the UserMap by iterating through the array, turning each row into a User object, and adding each User object to the Map.
1 | ... |
I tried adding a LoggingFilter to examine the actual request being sent to the server, but nothing jumped out at me. Then I added Mono Fiddler as a debugging proxy server and I saw it.
The request succeeded with “Content-Type: application/json”
The request failed with “Content-Type: application/json; charset=UTF-8”
A quick change to GsonWriter and my client requests started working.
1
2
3
4
5
6
7
8
9
10
11diff --git a/src/main/java/com/ideoplex/tutorial/GsonWriter.java b/src/main/java/com/ideoplex/tutorial/GsonWriter.java
index 9ef181d..95a12c3 100644
--- a/src/main/java/com/ideoplex/tutorial/GsonWriter.java
+++ b/src/main/java/com/ideoplex/tutorial/GsonWriter.java
@@ -42,7 +42,6 @@ public class GsonWriter<T> implements MessageBodyWriter<T> {
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws IOException, WebApplicationException {
- httpHeaders.get("Content-Type").add("charset=UTF-8");
entityStream.write(gson.toJson(t).getBytes("UTF-8"));
}
Of course, code changes always cascade. Now my selenium browser tests were failing. A little exploration in the browser developer tools revealed that JSON.parse was the culprit. Another code update and the tests were succeeding.
1 | diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html |
25 Oct
Autostart Jetty in Maven
01 Nov
Jersey Client with Gson
First, I’ve updated the pom.xml to automatically start the application in jetty during the maven process-test-classes phase and to automatically stop the jetty instance during the maven install phase. This insures that the webapp will be running and available in the maven test, integration-test and verify phases.
1 | diff --git a/pom.xml b/pom.xml index 318325d..47b6a2c 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,22 @@ <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>${jetty.version}</version> + <executions> + <execution> + <id>start-jetty</id> + <phase>process-test-classes</phase> + <goals> + <goal>start</goal> + </goals> + </execution> + <execution> + <id>stop-jetty</id> + <phase>install</phase> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> </plugin> <plugin> |
With this change, it is no longer necessary to start jetty via “mvn jetty:run” before executing the Selenium tests. I’ve also changed the name of the class running the Selenium tests from SetupTest to BrowserTest and made some minor updates:
1 | $ diff -U2 SetupTest.java BrowserTest.java --- SetupTest.java 2015-10-25 18:20:28.031091530 -0400 +++ BrowserTest.java 2015-10-25 18:21:37.855086967 -0400 @@ -17,5 +17,5 @@ -public class SetupTest { +public class BrowserTest { protected boolean ajaxWait = false; @@ -77,7 +77,6 @@ @Parameters({"browser","baseurl","waitajax"}) - @Test + @Test(invocationCount = 2, groups="browser") public void userCreate( String browser, String baseurl, String waitajax ) - throws Exception { WebDriver driver = "chrome".equalsIgnoreCase(browser) @@ -92,7 +91,14 @@ addUsers(driver); - Thread.sleep(10000); driver.quit(); } + @Test(dependsOnGroups = "browser") + public void pause() + throws Exception + { + System.out.println("Sleeping"); + Thread.sleep(10000); + } + } |
With this change, the “mvn test” output looks like this:
1 | $ mvn test [INFO] Scanning for projects... ... [INFO] jetty-9.3.0.M2 [INFO] Started o.e.j.m.p.JettyWebAppContext@1813f3e9{/,file:///Projects/webapp/jersey-gson/src/main/webapp/,AVAILABLE}{file:///Projects/webapp/jersey-gson/src/main/webapp/} [INFO] Started ServerConnector@806996{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} [INFO] Started @3741ms [INFO] Started Jetty Server [INFO] [INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ jersey-gson --- [INFO] Surefire report directory: /Projects/webapp/jersey-gson/target/surefire-reports ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.ideoplex.tutorial.BrowserTest Configuring TestNG with: TestNG652Configurator Compare george@example.com to No data available in table Compare john@example.com to No matching records found Compare thomas@example.com to No matching records found Compare james@example.com to No matching records found Compare james2@example.com to No matching records found Compare john2@example.com to No matching records found Compare andrew@example.com to No matching records found Compare martin@example.com to No matching records found Compare william@example.com to No matching records found Compare john3@example.com to No matching records found Compare james3@example.com to No matching records found Compare zachary@example.com to No matching records found Compare millard@example.com to No matching records found Compare abraham@example.com to No matching records found Compare george@example.com to george@example.com Compare john@example.com to john@example.com Compare thomas@example.com to thomas@example.com Compare james@example.com to james@example.com Compare james2@example.com to james2@example.com Compare john2@example.com to john2@example.com Compare andrew@example.com to andrew@example.com Compare martin@example.com to martin@example.com Compare william@example.com to william@example.com Compare john3@example.com to john3@example.com Compare james3@example.com to james3@example.com Compare zachary@example.com to zachary@example.com Compare millard@example.com to millard@example.com Compare abraham@example.com to abraham@example.com Sleeping Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 59.271 sec - in com.ideoplex.tutorial.BrowserTest Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 01:02 min [INFO] Finished at: 2015-10-25T20:06:20-04:00 [INFO] Final Memory: 24M/285M [INFO] ------------------------------------------------------------------------ |
16 Aug
DataTables, Bootstrap and Text Overflow
31 Oct jersey-gson Content-Type Woes
First, I’ve updated the pom.xml to automatically start the application in jetty during the maven process-test-classes phase and to automatically stop the jetty instance during the maven install phase. This insures that the webapp will be running and available in the maven test, integration-test and verify phases.
1 | diff --git a/pom.xml b/pom.xml index 318325d..47b6a2c 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,22 @@ <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>${jetty.version}</version> + <executions> + <execution> + <id>start-jetty</id> + <phase>process-test-classes</phase> + <goals> + <goal>start</goal> + </goals> + </execution> + <execution> + <id>stop-jetty</id> + <phase>install</phase> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> </plugin> <plugin> |