Skip to content
This repository has been archived by the owner on Jul 29, 2022. It is now read-only.

Commit

Permalink
Prep for react-router v4 merge, general improvements
Browse files Browse the repository at this point in the history
- Change Java top-level package.
- More comment documentation
- Fix error component
- Test that Nashorn can load the bundle when building
  • Loading branch information
pugnascotia committed Oct 13, 2016
1 parent c7dcea3 commit f6041cd
Show file tree
Hide file tree
Showing 28 changed files with 192 additions and 84 deletions.
4 changes: 4 additions & 0 deletions .editorconfig
Expand Up @@ -14,3 +14,7 @@ indent_size = 4
[*.java]
indent_style = tab
indent_size = 4

[*.xml]
indent_style = tab
indent_size = 4
30 changes: 25 additions & 5 deletions README.md
Expand Up @@ -17,7 +17,8 @@ project, but uses:
- [Redux](https://github.com/rackt/redux) to manage state, both in the
client and when rendering on the server.
- [react-router](https://github.com/rackt/react-router) for page routing,
on client and server
on client and server. Note that this is version 4, with a very different (and
simpler) API to previous versions.
- Linting integrated with Webpack via [eslint](https://github.com/MoOx/eslint-loader).
- Type checking with [Flow](https://flowtype.org/).

Expand All @@ -39,11 +40,30 @@ This isn't necessarily the best way to write a React application. Pull requests

Execute `mvn` if you have Maven already installed, or `./mvnw` if you don't. You'll need
[Java8 installed](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) either way at
a minimum version of 1.8.0_65.
a minimum version of `1.8.0_65`. Older versions have a bug that makes rendering
brutally slow.

Run webpack in hot-module reloading mode with: `npm run watch`.
Run webpack in hot-module reloading mode with: `npm start`.

## Conventions

Controllers that render views are suffixed with "Controller". REST endpoints are suffixed with "Resource",
and handle requests under "/api".
Controllers that render views are suffixed with `Controller`. REST endpoints are
suffixed with `Resource`, and handle requests under `/api`.

## Testing the webpack bundle

In order to pre-empty runtime errors with Nashorn loading the bundle, a test
script is executed by Maven during the `test-compile` phase, located at
`src/test/js/test_bundle.js`. If this script fails, you can diagnose the problem
by:

* Running a debug build with `npm run debug`. This runs webpack in a production
mode but without uglification.
* Run the script again: `jjs src/test/js/test_bundle.js`
* Look at the line in the bundle from the stacktrace and figure out the problem.

It's easy to create a bundle that's broken on the server by including code that
expects a DOM - and that includes the Webpack style loader. This is the root of
most problems. You should note that server-side rendering *does not* require a
DOM - which is why `src/main/resources/static/js/polyfill.js` doesn't provide
any `window` or `document` stubs.
29 changes: 25 additions & 4 deletions pom.xml
Expand Up @@ -63,7 +63,7 @@
</dependencies>

<build>
<defaultGoal>clean spring-boot:run</defaultGoal>
<defaultGoal>clean spring-boot:run</defaultGoal>
<plugins>

<plugin>
Expand All @@ -75,14 +75,35 @@
</configuration>
</plugin>

<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>Test bundle with Nashorn</id>
<!-- Ensure that Nashorn can load the bundle without errors. 'test' would arguably be a -->
<!-- more appropriate phase, but that doesn't get run with the default target. -->
<phase>test-compile</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>jjs</executable>
<commandlineArgs>${project.basedir}/src/test/js/test_bundle.js</commandlineArgs>
</configuration>
</execution>
</executions>
</plugin>

<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>0.0.26</version>

<configuration>
<installDirectory>.mvn</installDirectory>
</configuration>
<configuration>
<installDirectory>.mvn</installDirectory>
</configuration>

<executions>
<execution>
<id>Install Node and npm</id>
Expand Down
3 changes: 2 additions & 1 deletion src/main/flow/StyleStub.js
@@ -1,2 +1,3 @@
/* Stub for use with Flow */
/* Stub for use with Flow. Any imports of *.less files will be
* redirected here, essentially making them a no-op. */
module.export = {};
@@ -1,4 +1,4 @@
package uk.co.blackpepper;
package com.pugnascotia.reactdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
@@ -1,12 +1,17 @@
package uk.co.blackpepper.account;
package com.pugnascotia.reactdemo.account;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import static uk.co.blackpepper.utils.State.populateModel;
import static com.pugnascotia.reactdemo.utils.State.populateModel;

/**
* Handles a request for the signin page and renders the
* app. React Router takes care of showing the right page.
*/

@Controller
public class AccountController {
Expand Down
@@ -1,15 +1,22 @@
package uk.co.blackpepper.account;
package com.pugnascotia.reactdemo.account;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import com.pugnascotia.reactdemo.config.ajax.AjaxLogoutSuccessHandler;
import com.pugnascotia.reactdemo.utils.State;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.co.blackpepper.utils.State;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

/**
* Returns the user's current authentication status.
*
* @see AjaxLogoutSuccessHandler for how this is used.
*/

@RestController
@RequestMapping(value = "/api", produces = APPLICATION_JSON_VALUE)
public class AccountResource {
Expand Down
@@ -1,4 +1,4 @@
package uk.co.blackpepper.comments;
package com.pugnascotia.reactdemo.comments;

import lombok.Data;
import lombok.NoArgsConstructor;
Expand Down
@@ -0,0 +1,25 @@
package com.pugnascotia.reactdemo.comments;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import static com.pugnascotia.reactdemo.utils.State.populateModel;
import static org.springframework.web.bind.annotation.RequestMethod.GET;

/**
* Handles requests for the "add a comment" page. This is handled
* by our UI stack without any additional context.
*/

@Controller
public class CommentController {

@RequestMapping(value = "/add", method = GET)
public String index(Model model, HttpServletRequest request) {
populateModel(model, request);
return "index";
}
}
@@ -1,4 +1,4 @@
package uk.co.blackpepper.comments;
package com.pugnascotia.reactdemo.comments;

public interface CommentRepository {

Expand Down
@@ -1,37 +1,43 @@
package uk.co.blackpepper.comments;
package com.pugnascotia.reactdemo.comments;

import java.util.List;
import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.pugnascotia.reactdemo.utils.Functions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.co.blackpepper.utils.Functions;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

/**
* Handles creating new comments and fetching all comments via AJAX.
*/

@RestController
@RequestMapping(value = "/api", produces = APPLICATION_JSON_VALUE)
@Slf4j
public class CommentResource {

private static final Logger LOG = LoggerFactory.getLogger(CommentResource.class);
private final CommentRepository repository;

@Inject
private CommentRepository repository;
public CommentResource(CommentRepository repository) {
this.repository = repository;
}

@RequestMapping(path = "/comments", method = POST)
public Comment add(@RequestBody Comment comment) {
LOG.info("{}", comment);
log.info("{}", comment);
return repository.save(comment);
}

@RequestMapping(path = "/comments", method = GET)
public List<Comment> comments() {
// You shouldn't do this in a real app - you should page the data.
// You shouldn't do this in a real app - you should page the data!
return Functions.map(repository.findAll(), c -> c);
}
}
@@ -1,13 +1,12 @@
package uk.co.blackpepper.comments;
package com.pugnascotia.reactdemo.comments;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.PostConstruct;

import org.springframework.stereotype.Repository;

import javax.annotation.PostConstruct;

@Repository
public class InMemoryCommentRepository implements CommentRepository {

Expand Down
@@ -1,4 +1,4 @@
package uk.co.blackpepper.config;
package com.pugnascotia.reactdemo.config;

import java.io.IOException;
import javax.servlet.FilterChain;
Expand All @@ -7,13 +7,15 @@
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;
import uk.co.blackpepper.utils.Cookies;
import com.pugnascotia.reactdemo.utils.Cookies;

public class CsrfHeaderFilter extends OncePerRequestFilter {
/**
* This filter ensures that if a request does not supply a CSRF token in a cookie,
* or if the token is not up-to-date, we set it in our response so that subsequent
* requests can succeed.
*/

/** Ensure that if a request does not supply a CSRF token in a cookie, or
* if the token is not up-to-date, we set it in our response so that subsequent
* requests can succeed. */
class CsrfHeaderFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Expand Down
@@ -1,7 +1,8 @@
package uk.co.blackpepper.config;
package com.pugnascotia.reactdemo.config;

import javax.inject.Inject;

import com.pugnascotia.reactdemo.config.ajax.AjaxAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -11,9 +12,8 @@
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import uk.co.blackpepper.config.ajax.AjaxAuthenticationFailureHandler;
import uk.co.blackpepper.config.ajax.AjaxAuthenticationSuccessHandler;
import uk.co.blackpepper.config.ajax.AjaxLogoutSuccessHandler;
import com.pugnascotia.reactdemo.config.ajax.AjaxAuthenticationFailureHandler;
import com.pugnascotia.reactdemo.config.ajax.AjaxLogoutSuccessHandler;

@Configuration
@EnableWebSecurity
Expand All @@ -39,12 +39,17 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}

/**
* Specify the path that Spring Security will completely ignore. This is distinct
* from paths that are available to all users.
* Specify the paths that Spring Security will completely ignore. This is distinct
* from paths that are available to all users. Static, public resources are ideal
* candidates.
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/app/**", "/js/**", "/favicon.ico");
web.ignoring().antMatchers(
"/app/**",
"/js/**",
"/favicon.ico"
);
}

@Override
Expand Down
@@ -1,4 +1,4 @@
package uk.co.blackpepper.config;
package com.pugnascotia.reactdemo.config;

import javax.script.ScriptEngine;

Expand All @@ -18,7 +18,8 @@ public class ViewConfig {
* These are scripts needed to render a React application.
* <ul>
* <li><code>polyfill.js</code> - implements some standard functions from a browser / NodeJS environment</li>
* <li><code>ejs.min.js</code> - EJS, a small JavaScript rendering library</li>
* <li><code>ejs.min.js</code> - EJS, a small JavaScript rendering library. This is copied from another
* demo project, and I don't know the original provenance.</li>
* <li><code>render.js</code> - code that invokes EJS with the correct values</li>
* <li><code>bundle.js</code> - all our application code, bundled up by Webpack</li>
* </ul>
Expand Down
@@ -1,4 +1,4 @@
package uk.co.blackpepper.config.ajax;
package com.pugnascotia.reactdemo.config.ajax;

import java.io.IOException;
import javax.servlet.ServletException;
Expand Down
@@ -1,4 +1,4 @@
package uk.co.blackpepper.config.ajax;
package com.pugnascotia.reactdemo.config.ajax;

import java.io.IOException;
import javax.inject.Inject;
Expand All @@ -11,16 +11,23 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import uk.co.blackpepper.utils.Cookies;
import uk.co.blackpepper.utils.State;
import com.pugnascotia.reactdemo.utils.Cookies;
import com.pugnascotia.reactdemo.utils.State;

/**
* A handler that returns HTTP 200 OK for successful AJAX authentications,
* and includes the user's roles in the response body.
*/

@Component
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Inject
private ObjectMapper mapper;
private final ObjectMapper mapper;

/** Return 200 OK for successful AJAX authentications, plus user's roles */
@Inject
public AjaxAuthenticationSuccessHandler(ObjectMapper mapper) {
this.mapper = mapper;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Expand Down

0 comments on commit f6041cd

Please sign in to comment.