diff --git a/basic/package.json b/basic/package.json index dbb9c5a..4c4ac56 100644 --- a/basic/package.json +++ b/basic/package.json @@ -20,6 +20,8 @@ }, "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest", "dependencies": { + "@material-ui/core": "^4.12.3", + "eslint": "^8.11.0", "react": "^16.5.2", "react-dom": "^16.5.2", "rest": "^1.3.1" diff --git a/basic/src/main/java/com/greglturnquist/FormApplication.java b/basic/src/main/java/com/greglturnquist/FormApplication.java new file mode 100644 index 0000000..c580a42 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/FormApplication.java @@ -0,0 +1,42 @@ +package com.greglturnquist; + +import com.greglturnquist.model.form.Form; +import com.greglturnquist.model.form.Question; +import com.greglturnquist.repository.FormRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class FormApplication { + + private static final Logger log = LoggerFactory.getLogger(FormApplication.class); + + public static void main(String[] args) { + SpringApplication.run(FormApplication.class, args); + } + + @Bean + public CommandLineRunner run(FormRepository repository) { + return (args) -> { + Form form = new Form(); + + form.addQuestion(new Question()); + form.addQuestion(new Question("what day is it?")); + form.addQuestion(new Question("Do you enjoy sports?")); + + repository.save(form); + + log.info("Created new Form"); + + + for (Form f : repository.findAll()) { + log.info(f.getId().toString()); + log.info(f.toString()); + } + }; + } +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/controller/AnswerController.java b/basic/src/main/java/com/greglturnquist/controller/AnswerController.java new file mode 100644 index 0000000..b4142f5 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/controller/AnswerController.java @@ -0,0 +1,18 @@ +package com.greglturnquist.controller; + + +import com.greglturnquist.repository.AnswerRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; + +@Controller +public class AnswerController { + + @Autowired + private AnswerRepository answerRepository; + + public AnswerController(AnswerRepository answerRepository){ + this.answerRepository = answerRepository; + } + +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/controller/WebController.java b/basic/src/main/java/com/greglturnquist/controller/WebController.java new file mode 100644 index 0000000..cda8c0e --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/controller/WebController.java @@ -0,0 +1,90 @@ +package com.greglturnquist.controller; + +import com.greglturnquist.model.form.Form; +import com.greglturnquist.model.form.Question; +import com.greglturnquist.repository.FormRepository; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +public class WebController { + + private final FormRepository repository; + + public WebController(FormRepository repository) { + this.repository = repository; + } + + @PostMapping("/form") + public Form createForm(@RequestBody(required = false) List questionList) { + Form a = new Form(); + if(questionList != null) { + for(Question b : questionList) { + a.addQuestion(b); + } + } + repository.save(a); + + return a; + } + + @GetMapping("/form/{id}") + public Form getForm(@PathVariable String id) { + Optional
response = repository.findById(UUID.fromString(id)); + return response.orElse(null); + } + + @GetMapping("/form") + public Form getAllForms() { + Iterable response = repository.findAll(); + List temp = new ArrayList(); + for (Form f : response) { + temp.add(f); + } + //returning first index just for the first milestone + return temp.get(0); + } + + @PostMapping("/form/{id}") + public PrimitiveResponse createQuestion(@PathVariable String id, @RequestBody Question question) { + Optional response = repository.findById(UUID.fromString(id)); + if(response.isEmpty()) { + return new PrimitiveResponse<>("success", false); + } + Form book = response.get(); + book.addQuestion(question); + repository.save(book); + return new PrimitiveResponse<>("success", true); + } + + @DeleteMapping("/form/{id}/question/{questionId}") + public PrimitiveResponse deleteQuestion(@PathVariable String id, @PathVariable String questionId) { + Optional response = repository.findById(UUID.fromString(id)); + if(response.isEmpty()) { + return new PrimitiveResponse<>("success", false); + } + Form book = response.get(); + if(book.removeQuestion(UUID.fromString(questionId))) { + repository.save(book); + return new PrimitiveResponse<>("success", true); + } + return new PrimitiveResponse<>("success", false); + } + +} + + +class PrimitiveResponse { + + private final Map body; + + public PrimitiveResponse(String key, T value) { + this.body = new HashMap<>(1); + this.body.put(key, value); + } + + public Map getBody() { + return this.body; + } +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/controller/WebUIController.java b/basic/src/main/java/com/greglturnquist/controller/WebUIController.java new file mode 100644 index 0000000..e7a0699 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/controller/WebUIController.java @@ -0,0 +1,174 @@ +package com.greglturnquist.controller; + +import com.greglturnquist.model.Answer; +import com.greglturnquist.model.form.Form; +import com.greglturnquist.model.form.Question; +import com.greglturnquist.model.form.data.AnswerAndQuestion; +import com.greglturnquist.model.form.data.DataForm; +import com.greglturnquist.model.form.data.QuestionForm; +import com.greglturnquist.repository.FormRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +@Controller +public class WebUIController { + + private final FormRepository repository; + private static boolean closed = true; + + public static String decodeValue(String value) { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex.getCause()); + } + } + + public WebUIController(FormRepository repository) { + this.repository = repository; + } + + @GetMapping("/ui") + public String selectForm(Model model) { + model.addAttribute("dataForm", new DataForm()); + return "dataForm"; + } + + @PostMapping("/ui") + public String getForm(@ModelAttribute DataForm dataForm, Model model) { + Optional result = repository.findById(UUID.fromString(dataForm.getContent())); + + model.addAttribute("Form", result.orElse(null)); + return "result"; + } + + @GetMapping("/create") + public String createForm(Model model) { + Form form = new Form(); + repository.save(form); + + model.addAttribute("Form", form); + return "result"; + } + @PostMapping(value = "/submission") + public String submitForm(@RequestBody String body) throws JsonProcessingException { + System.out.println(body); + String decodedValue = decodeValue(body); + decodedValue = decodedValue.substring(0, decodedValue.length() - 1); + System.out.println(decodedValue); + List answerList = Arrays.asList(decodedValue.split(",")); + for(String s : answerList){ + System.out.println(s); + } + System.out.println(answerList); + Iterable response = repository.findAll(); + List temp = new ArrayList(); + for (Form f : response) { + temp.add(f); + } + Form f = temp.get(0); +// + + List questionList = f.getQuestions(); +// for(Question q: questionList){ +// System.out.println(q); +// Answer answer = new Answer("hey"); +// q.addAnswerList(answer); +// answer.setQuestion(q); +// } + for(int i = 0; i < questionList.size(); i++){ + Answer answer = new Answer(answerList.get(i)); + questionList.get(i).addAnswerList(answer); + answer.setQuestion(questionList.get(i)); + } + repository.save(f); + return "Thanks"; + } + + @GetMapping("/question") + public String inputQuestionData(Model model) { + model.addAttribute("questionForm", new QuestionForm()); + return "question"; + } + + @PostMapping("/question") + public String createQuestionInfo(@ModelAttribute QuestionForm questionForm, Model model) { + Optional result = repository.findById(UUID.fromString(questionForm.getId())); + + result.ifPresent(form -> { + form.addQuestion(new Question(questionForm.getValue())); + repository.save(form); + }); + + model.addAttribute("questionForm", result.get()); + + return "result"; + } + + @GetMapping("/admin") + public String displayAdmin() { + return "admin"; + } + + + @GetMapping("/survey") + public String displaySurvey(Model model) { + if (closed != true){ + return closeForm(model); + } + model.addAttribute("closed", closed); + return "survey"; + } + + @GetMapping("/survey2") + public String displaySurvey2(Model model) { +// AnswerList answerList = new AnswerList(); +// answerList.setQuestion("hello"); + List answerList = new ArrayList<>(); + Form form = new Form(); + form.addQuestion(new Question()); + form.addQuestion(new Question("what day is it?")); + form.addQuestion(new Question("Do you enjoy sports?")); + List questionList = form.getQuestions(); + for(Question question : questionList){ + AnswerAndQuestion answer = new AnswerAndQuestion(); + answer.setQuestion(question.getValue()); + answerList.add(answer); + } +// model.addAttribute("Form", form); + model.addAttribute("answerList", answerList); + + return "survey2"; + } + + + @GetMapping("/controlPanel") + public String displayControlPanel() { + return "controlPanel"; + } + + @GetMapping("/closeForm") + public String closeForm(Model model) { + this.closed = false; +// Iterable response = repository.findAll(); +// List temp = new ArrayList(); +// for (Form f : response) { +// temp.add(f); +// } +// Form form = temp.get(0); +// List questionList = form.getQuestions(); +// model.addAttribute("questions", questionList); + return "answerResults"; + } + +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/model/Answer.java b/basic/src/main/java/com/greglturnquist/model/Answer.java new file mode 100644 index 0000000..4bf612b --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/Answer.java @@ -0,0 +1,68 @@ +package com.greglturnquist.model; + +import com.greglturnquist.model.form.Question; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class Answer { + + @Id + @GeneratedValue + private UUID id; + + private String answer; + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "question_id") + private Question question; + + public Answer() { + } + + public Answer(String answer) { + this.answer = answer; + } + + public UUID getId() { + return id; + } + + public String getAnswer() { + return answer; + } + + public void setAnswer(String answer) { + this.answer = answer; + } + + public void setQuestion(Question question){ + this.question = question; + } + + public Question getQuestion(){ + return question; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (getClass() != o.getClass()) + return false; + Answer other = (Answer) o; + if (answer == null) { + return other.answer == null; + } else return answer.equals(other.answer); + } + + public String serialize() { + return answer; + } + +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/model/form/Form.java b/basic/src/main/java/com/greglturnquist/model/form/Form.java new file mode 100644 index 0000000..a68d9da --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/form/Form.java @@ -0,0 +1,146 @@ +package com.greglturnquist.model.form; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import java.util.UUID; + + +@Entity +public class Form { + + // unique id for each From + @Id + private UUID id; + + // One to many relationship with questions + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "form") + private List questions; + + /** + * Initialize id and questions arraylist to create a Form which will hold questions + */ + + public Form() { + this.id = UUID.randomUUID(); + questions = new ArrayList<>(); + } + + /** + * Add question to form. + * @param question Question to be added to Form. + */ + public void addQuestion(Question question) { + question.setForm(this); + this.questions.add(question); + } + + + /** + * Get id of Form. + * @return UUID representation of id of Form. + */ + public UUID getId() { + return id; + } + + /** + * Set id of Form + * @param id UUID which the Form id will be set to. + */ + public void setId(UUID id) { + this.id = id; + } + + /** + * Gets all the Questions of the Form. + * @return A List containing the Questions of the Form. + */ + @OneToMany(mappedBy = "form") + public List getQuestions() { + return this.questions; + } + + /** + * Set the List of Questions the Form has. + * @param questions The List of Questions the Form will have. + */ + public void setQuestions(List questions) { + this.questions = questions; + } + + /** + * Remove Question based of index. + * @param index The index of the Question to be removed. + */ + public void removeQuestion(int index) { + questions.remove(index); + } + + + /** + * Remove Question based off UUID. + * @param id UUID of Question be removed. + * @return a boolean representing whether the specified Question was removed. + */ + public boolean removeQuestion(UUID id) { + for(int i = 0; i < questions.size(); i++) { + if(questions.get(i).getId().equals(id)) { + removeQuestion(i); + return true; + } + } + return false; + } + + + @Override + public String toString() { + StringJoiner j = new StringJoiner("\n"); + + for(Question question : questions) { + j.add(question.toString()); + } + + return j.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (getClass() != o.getClass()) + return false; + Form other = (Form) o; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (questions == null) { + return other.questions == null; + } else return questions.equals(other.questions); + } + + + /** + * Serial representation of Form class. + * @return Serial representation of Form class. + */ + public String serialize() { + StringJoiner b = new StringJoiner(",", + "{" + + "\"data\": [" + + "\"type\": \"form\"" + + "\"id\": " + this.id + + "\"attributes\": {[", + "]}]}"); + for(Question question : questions) { + b.add(question.serialize()); + } + return b.toString(); + } +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/model/form/Question.java b/basic/src/main/java/com/greglturnquist/model/form/Question.java new file mode 100644 index 0000000..302953a --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/form/Question.java @@ -0,0 +1,147 @@ +package com.greglturnquist.model.form; + +import com.greglturnquist.model.Answer; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + + +@Entity +public class Question { + + //unique id for each Question + @Id + private UUID id; + + //Question text + private String value; + + //List of survey answers for question + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "question") + private List answerList; + + + //Form the question is associated with + @JsonIgnore + @ManyToOne + @JoinColumn(name = "form_id") + private Form form; + + /** + * Default constructor for Question. + */ + public Question() { + this("What is your name?"); + } + + /** + * Create Question with specified string as the question. + * @param value String containing the contents of the question. + */ + public Question(String value) { + this.value = value; + this.id = UUID.randomUUID(); + this.answerList = new ArrayList<>(); + } + + + /** + * Set id of Question. + * @param id UUID which the id will be set to. + */ + public void setId(UUID id) { + this.id = id; + } + + /** + * Set question text. + * @param value New question text. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Add answer to list. + * @param answer Answer to be added to List. + */ + public void addAnswerList(Answer answer){ + answer.setQuestion(this); + this.answerList.add(answer); + } + + /** + * Get Answers for this Question. + * @return a List containing the Answers for this Question. + */ + public List getAnswerList() { + return answerList; + } + + /** + * Get id of Question. + * @return UUID of Question. + */ + public UUID getId() { + return this.id; + } + + /** + * Gets question string. + * @return String containing question text. + */ + public String getValue() { + return this.value; + } + + /** + * Gets Form associated with Question. + * @return Form associated with Question. + */ + public Form getForm() { + return form; + } + + /** + * Set the Form which is associated with this Question. + * @param form The Form which holds this question. + */ + public void setForm(Form form) { + this.form = form; + } + + @Override + public String toString() { + return this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (getClass() != o.getClass()) + return false; + Question other = (Question) o; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (value == null) { + return other.value == null; + } else return value.equals(other.value); + } + + /** + * Serial representation of Question class. + * @return Serial representation of Question class. + */ + public String serialize() { + return value; + } +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/model/form/data/AnswerAndQuestion.java b/basic/src/main/java/com/greglturnquist/model/form/data/AnswerAndQuestion.java new file mode 100644 index 0000000..728a0e0 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/form/data/AnswerAndQuestion.java @@ -0,0 +1,34 @@ +package com.greglturnquist.model.form.data; + +public class AnswerAndQuestion { + + + private String content; + + private String question; + + public String getQuestion(){ + return this.question; + } + + public String getContent() { + return this.content; + } + + public void setQuestion(String question){ + this.question = question; + } + + public void setContent(String content){ + this.content = content; + } + + + //loop thru list + //call current form, + //check if currentForm == to current question + //if so, update that questions answer. + //store form back in repo + +} + diff --git a/basic/src/main/java/com/greglturnquist/model/form/data/DataForm.java b/basic/src/main/java/com/greglturnquist/model/form/data/DataForm.java new file mode 100644 index 0000000..3c96e95 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/form/data/DataForm.java @@ -0,0 +1,15 @@ +package com.greglturnquist.model.form.data; + +public class DataForm { + + private String content; + + public void setContent(String content) { + this.content = content; + } + + public String getContent() { + return this.content; + } + +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/model/form/data/QuestionForm.java b/basic/src/main/java/com/greglturnquist/model/form/data/QuestionForm.java new file mode 100644 index 0000000..f7dad04 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/model/form/data/QuestionForm.java @@ -0,0 +1,25 @@ +package com.greglturnquist.model.form.data; + +public class QuestionForm { + + private String id; + private String value; + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java deleted file mode 100644 index 017ee7b..0000000 --- a/basic/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component // <1> -public class DatabaseLoader implements CommandLineRunner { // <2> - - private final EmployeeRepository repository; - - @Autowired // <3> - public DatabaseLoader(EmployeeRepository repository) { - this.repository = repository; - } - - @Override - public void run(String... strings) throws Exception { // <4> - this.repository.save(new Employee("Frodo", "Baggins", "ring bearer")); - } -} -// end::code[] diff --git a/basic/src/main/java/com/greglturnquist/payroll/Employee.java b/basic/src/main/java/com/greglturnquist/payroll/Employee.java deleted file mode 100644 index 80ec33c..0000000 --- a/basic/src/main/java/com/greglturnquist/payroll/Employee.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Entity // <1> -public class Employee { - - private @Id @GeneratedValue Long id; // <2> - private String firstName; - private String lastName; - private String description; - - private Employee() {} - - public Employee(String firstName, String lastName, String description) { - this.firstName = firstName; - this.lastName = lastName; - this.description = description; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Employee employee = (Employee) o; - return Objects.equals(id, employee.id) && - Objects.equals(firstName, employee.firstName) && - Objects.equals(lastName, employee.lastName) && - Objects.equals(description, employee.description); - } - - @Override - public int hashCode() { - - return Objects.hash(id, firstName, lastName, description); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - @Override - public String toString() { - return "Employee{" + - "id=" + id + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", description='" + description + '\'' + - '}'; - } -} -// end::code[] diff --git a/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java deleted file mode 100644 index 17cbee8..0000000 --- a/basic/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.data.repository.CrudRepository; - -/** - * @author Greg Turnquist - */ -// tag::code[] -public interface EmployeeRepository extends CrudRepository { // <1> - -} -// end::code[] diff --git a/basic/src/main/java/com/greglturnquist/payroll/HomeController.java b/basic/src/main/java/com/greglturnquist/payroll/HomeController.java deleted file mode 100644 index 98f5711..0000000 --- a/basic/src/main/java/com/greglturnquist/payroll/HomeController.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Controller // <1> -public class HomeController { - - @RequestMapping(value = "/") // <2> - public String index() { - return "index"; // <3> - } - -} -// end::code[] diff --git a/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java deleted file mode 100644 index 7b2fc31..0000000 --- a/basic/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@SpringBootApplication -public class ReactAndSpringDataRestApplication { - - public static void main(String[] args) { - SpringApplication.run(ReactAndSpringDataRestApplication.class, args); - } -} -// end::code[] \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/repository/AnswerRepository.java b/basic/src/main/java/com/greglturnquist/repository/AnswerRepository.java new file mode 100644 index 0000000..f08ab0a --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/repository/AnswerRepository.java @@ -0,0 +1,8 @@ +package com.greglturnquist.repository; + +import com.greglturnquist.model.Answer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AnswerRepository extends JpaRepository { + Answer findById(long id); +} \ No newline at end of file diff --git a/basic/src/main/java/com/greglturnquist/repository/FormRepository.java b/basic/src/main/java/com/greglturnquist/repository/FormRepository.java new file mode 100644 index 0000000..25e8ac9 --- /dev/null +++ b/basic/src/main/java/com/greglturnquist/repository/FormRepository.java @@ -0,0 +1,26 @@ +package com.greglturnquist.repository; + +import com.greglturnquist.model.form.Form; +import com.greglturnquist.model.form.Question; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +import java.util.Optional; +import java.util.UUID; + +/** + * Used to load/save Form in database. + */ +@RepositoryRestResource(collectionResourceRel = "forms", path = "forms") +public interface FormRepository extends PagingAndSortingRepository { + + //Save method. + void save(S s); + + //Find by id method. + Optional findById(UUID id); + + //Delete by id method. + void deleteById(UUID id); + +} \ No newline at end of file diff --git a/basic/src/main/js/Form.js b/basic/src/main/js/Form.js new file mode 100644 index 0000000..cc6e5b1 --- /dev/null +++ b/basic/src/main/js/Form.js @@ -0,0 +1,17 @@ +const React = require('react'); // <1> +import Question from './Question' + + +export default class Form extends React.Component{ + render() { + const questionsArr = this.props.form?.questions ?? [] + const questions = questionsArr.map(question => + + ); + return ( +
+ {questions} +
+ ) + } +} \ No newline at end of file diff --git a/basic/src/main/js/Question.js b/basic/src/main/js/Question.js new file mode 100644 index 0000000..734d308 --- /dev/null +++ b/basic/src/main/js/Question.js @@ -0,0 +1,11 @@ +const React = require('react'); // <1> + +export default class Question extends React.Component{ + render() { + return ( +
+
{this.props.question.value}
+
+ ) + } +} \ No newline at end of file diff --git a/basic/src/main/js/app.js b/basic/src/main/js/app.js index dea346c..5270d5a 100644 --- a/basic/src/main/js/app.js +++ b/basic/src/main/js/app.js @@ -1,4 +1,4 @@ -'use strict'; +import Form from './Form' // tag::vars[] const React = require('react'); // <1> @@ -11,57 +11,56 @@ class App extends React.Component { // <1> constructor(props) { super(props); - this.state = {employees: []}; + this.state = {forms: []}; } componentDidMount() { // <2> - client({method: 'GET', path: '/api/employees'}).done(response => { - this.setState({employees: response.entity._embedded.employees}); + client({method: 'GET', path: '/form'}).done(response => { + this.setState({form: response.entity}); }); } render() { // <3> return ( - + ) } } // end::app[] -// tag::employee-list[] -class EmployeeList extends React.Component{ - render() { - const employees = this.props.employees.map(employee => - - ); - return ( - - - - - - - - {employees} - -
First NameLast NameDescription
- ) - } -} -// end::employee-list[] - -// tag::employee[] -class Employee extends React.Component{ - render() { - return ( - - {this.props.employee.firstName} - {this.props.employee.lastName} - {this.props.employee.description} - - ) - } -} +// class EmployeeList extends React.Component{ +// render() { +// const employees = this.props.employees.map(employee => +// +// ); +// return ( +// +// +// +// +// +// +// +// {employees} +// +//
First NameLast NameDescription
+// ) +// } +// } +// // end::employee-list[] +// +// // tag::employee[] +// class Employee extends React.Component{ +// render() { +// return ( +// +// {this.props.employee.firstName} +// {this.props.employee.lastName} +// {this.props.employee.description} +// +// ) +// } +// } // end::employee[] // tag::render[] diff --git a/conditional/.mvn/wrapper/MavenWrapperDownloader.java b/conditional/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3..0000000 --- a/conditional/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/conditional/.mvn/wrapper/maven-wrapper.jar b/conditional/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a..0000000 Binary files a/conditional/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/conditional/.mvn/wrapper/maven-wrapper.properties b/conditional/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572..0000000 --- a/conditional/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/conditional/README.adoc b/conditional/README.adoc deleted file mode 100644 index a3494e6..0000000 --- a/conditional/README.adoc +++ /dev/null @@ -1,166 +0,0 @@ -[[react-and-spring-data-rest-part-3]] -= Part 3 - Conditional Operations -:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master - -In the <>, you found out how to turn on Spring Data REST's hypermedia controls, have the UI navigate by paging, and dynamically resize based on changing the page size. You added the ability to create and delete employees and have the pages adjust. But no solution is complete without taking into consideration updates made by other users on the same bit of data you are currently editing. - -Feel free to {sourcedir}/conditional[grab the code] from this repository and follow along. This section is based on the previous section but with extra features added. - -== To PUT or Not to PUT? That is the Question. - -When you fetch a resource, the risk is that it might go stale if someone else updates it. To deal with this, Spring Data REST integrates two technologies: versioning of resources and ETags. - -By versioning resources on the backend and using ETags in the frontend, it is possible to conditionally `PUT` a change. In other words, you can detect whether a resource has changed and prevent a `PUT` (or a `PATCH`) from stomping on someone else's update. - -== Versioning REST Resources - -To support versioning of resources, define a version attribute for your domain objects that need this type of protection. The following listing shows how to do so for the `Employee` object: - -.src/main/java/com/greglturnquist/payroll/Employee.java -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/Employee.java[tag=code] ----- -==== - -* The `version` field is annotated with `javax.persistence.Version`. It causes a value to be automatically stored and updated every time a row is inserted and updated. - -When fetching an individual resource (not a collection resource), Spring Data REST automatically adds an https://tools.ietf.org/html/rfc7232#section-2.3[ETag response header] with the value of this field. - -== Fetching Individual Resources and Their Headers - -In the <>, you used the collection resource to gather data and populate the UI's HTML table. With Spring Data REST, the `_embedded` data set is considered a preview of data. While useful for glancing at data, to get headers like ETags, you need to fetch each resource individually. - -In this version, `loadFromServer` is updated to fetch the collection. Then you can use the URIs to retrieve each individual resource: - -.src/main/js/app.js - Fetching each resource -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=follow-2] ----- - -<1> The `follow()` function goes to the `employees` collection resource. - -<2> The first `then(employeeCollection => ...)` clause creates a call to fetch JSON Schema data. This has an inner then clause to store the metadata and navigational links in the `` component. -+ -Notice that this embedded promise returns the `employeeCollection`. That way, the collection can be passed onto the next call, letting you grab the metadata along the way. - -<3> The second `then(employeeCollection => ...)` clause converts the collection of employees into an array of `GET` promises to fetch each individual resource. *This is what you need to fetch an ETag header for each employee.* - -<4> The `then(employeePromises => ...)` clause takes the array of `GET` promises and merges them into a single promise with `when.all()`, which is resolved when all the GET promises are resolved. - -<5> `loadFromServer` wraps up with `done(employees => ...)` where the UI state is updated using this amalgamation of data. -==== - -This chain is implemented in other places as well. For example, `onNavigate()` (which is used to jump to different pages) has been updated to fetch individual resources. Since it is mostly the same as what is shown here, it has been left out of this section. - -== Updating Existing Resources - -In this section, you are adding an `UpdateDialog` React component to edit existing employee records: - -.src/main/js/app.js - UpdateDialog component -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=update-dialog] ----- -==== - -This new component has both a `handleSubmit()` function and the expected `render()` function, similar to the `` component. - -We dig into these functions in reverse order and first look at the `render()` function. - -=== Rendering - -This component uses the same CSS/HTML tactics to show and hide the dialog as the `` from the previous section. - -It converts the array of JSON Schema attributes into an array of HTML inputs, wrapped in paragraph elements for styling. This is also the same as the `` with one difference: The fields are loaded with `this.props.employee`. In the `CreateDialog` component, the fields are empty. - -The `id` field is built differently. There is only one `CreateDialog` link on the entire UI, but a separate `UpdateDialog` link for every row displayed. Hence, the `id` field is based on the `self` link's URI. This is used in the `
` element's React `key`, the HTML anchor tag, and the hidden pop-up. - -=== Handling User Input - -The submit button is linked to the component's `handleSubmit()` function. This handily uses `React.findDOMNode()` to extract the details of the pop-up by using https://facebook.github.io/react/docs/more-about-refs.html[React refs]. - -After the input values are extracted and loaded into the `updatedEmployee` object, the top-level `onUpdate()` method is invoked. This continues React's style of one-way binding where the functions to call are pushed from upper-level components into the lower-level ones. This way, state is still managed at the top. - -== Conditional PUT - -So you have gone to all this effort to embed versioning in the data model. Spring Data REST has served up that value as an ETag response header. Here is where you get to put it to good use: - -.src/main/js/app.js - onUpdate function -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=update] ----- -==== - -A `PUT` with an https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24[`If-Match` request header] causes Spring Data REST to check the value against the current version. If the incoming `If-Match` value does not match the data store's version value, Spring Data REST will fail with an `HTTP 412 Precondition Failed`. - -NOTE: The specification for https://promisesaplus.com/[Promises/A+] actually defines their API as `then(successFunction, errorFunction)`. So far, you have seen it used only with success functions. In the preceding code fragment, there are two functions. The success function invokes `loadFromServer`, while the error function displays a browser alert about the stale data. - -== Putting It All Together - -With your `UpdateDialog` React component defined and nicely linked to the top-level `onUpdate` function, the last step is to wire it into the existing layout of components. - -The `CreateDialog` created in the previous section was put at the top of the `EmployeeList` because there is only one instance. However, `UpdateDialog` is tied directly to specific employees. So you can see it plugged in below in the `Employee` React component: - -.src/main/js/app.js - Employee with UpdateDialog -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=employee] ----- -==== - -In this section, you switch from using the collection resource to using individual resources. The fields for an employee record are now found at `this.props.employee.entity`. It gives us access to `this.props.employee.headers`, where we can find ETags. - -There are other headers supported by Spring Data REST (such as https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29[Last-Modified]) that are not part of this series. So structuring your data this way is handy. - -IMPORTANT: The structure of `.entity` and `.headers` is only pertinent when using https://github.com/cujojs/rest[rest.js] as the REST library of choice. If you use a different library, you will have to adapt as necessary. - -== Seeing Things in Action - -To see the modified application work: - -. Start the application by running `./mvnw spring-boot:run`. -. Open a browser tab and navigate to http://localhost:8080. -+ -You should see a page similar to the following image: -+ -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-1.png[] -+ -. Pull up the edit dialog for Frodo. -. Open another tab in your browser and pull up the same record. -. Make a change to the record in the first tab. -. Try to make a change in the second tab. -+ -You should see the browser tabs change, as the following images show -+ -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-2.png[] - -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/conditional/images/conditional-3.png[] - -With these modifications, you have increased data integrity by avoiding collisions. - -== Review - -In this section: - -* You configured your domain model with a `@Version` field for JPA-based optimistic locking. -* You adjusted the frontend to fetch individual resources. -* You plugged the ETag header from an individual resource into an `If-Match` request header to make PUTs conditional. -* You coded a new `UpdateDialog` for each employee shown on the list. - -With this plugged in, itis easy to avoid colliding with other users or overwriting their edits. - -Issues? - -It is certainly nice to know when you are editing a bad record. But is it best to wait until you click "Submit" to find out? - -The logic to fetch resources is very similar in both `loadFromServer` and `onNavigate`. Do you see ways to avoid duplicate code? - -You put the JSON Schema metadata to good use in building up the `CreateDialog` and the `UpdateDialog` inputs. Do you see other places to use the metadata to makes things more generic? Imagine you wanted to add five more fields to `Employee.java`. What would it take to update the UI? diff --git a/conditional/images/conditional-1.png b/conditional/images/conditional-1.png deleted file mode 100644 index edf520b..0000000 Binary files a/conditional/images/conditional-1.png and /dev/null differ diff --git a/conditional/images/conditional-2.png b/conditional/images/conditional-2.png deleted file mode 100644 index 8cf8a24..0000000 Binary files a/conditional/images/conditional-2.png and /dev/null differ diff --git a/conditional/images/conditional-3.png b/conditional/images/conditional-3.png deleted file mode 100644 index 13f3e35..0000000 Binary files a/conditional/images/conditional-3.png and /dev/null differ diff --git a/conditional/mvnw b/conditional/mvnw deleted file mode 100755 index a16b543..0000000 --- a/conditional/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/conditional/mvnw.cmd b/conditional/mvnw.cmd deleted file mode 100644 index c8d4337..0000000 --- a/conditional/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/conditional/package.json b/conditional/package.json deleted file mode 100644 index dbb9c5a..0000000 --- a/conditional/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "spring-data-rest-and-reactjs", - "version": "0.1.0", - "description": "Demo of ReactJS + Spring Data REST", - "repository": { - "type": "git", - "url": "git@github.com:spring-guides/tut-react-and-spring-data-rest.git" - }, - "keywords": [ - "rest", - "hateoas", - "spring", - "data", - "react" - ], - "author": "Greg L. Turnquist", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues" - }, - "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest", - "dependencies": { - "react": "^16.5.2", - "react-dom": "^16.5.2", - "rest": "^1.3.1" - }, - "scripts": { - "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js" - }, - "devDependencies": { - "@babel/core": "^7.1.0", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "babel-loader": "^8.0.2", - "webpack": "^4.19.1", - "webpack-cli": "^3.1.0" - } -} diff --git a/conditional/pom.xml b/conditional/pom.xml deleted file mode 100644 index 0674c8d..0000000 --- a/conditional/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - 4.0.0 - - - com.greglturnquist - react-and-spring-data-rest - 0.0.1-SNAPSHOT - - - react-and-spring-data-rest-conditional - 0.0.1-SNAPSHOT - - React.js and Spring Data REST - Conditional - An SPA with ReactJS in the frontend and Spring Data REST in the backend - - - UTF-8 - 1.8 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-data-rest - - - org.springframework.boot - spring-boot-devtools - - - com.h2database - h2 - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - com.github.eirslett - frontend-maven-plugin - - - - - diff --git a/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java deleted file mode 100644 index cb339c2..0000000 --- a/conditional/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -public class DatabaseLoader implements CommandLineRunner { - - private final EmployeeRepository repository; - - @Autowired - public DatabaseLoader(EmployeeRepository repository) { - this.repository = repository; - } - - @Override - public void run(String... strings) throws Exception { - - this.repository.save(new Employee("Frodo", "Baggins", "ring bearer")); - this.repository.save(new Employee("Bilbo", "Baggins", "burglar")); - this.repository.save(new Employee("Gandalf", "the Grey", "wizard")); - this.repository.save(new Employee("Samwise", "Gamgee", "gardener")); - this.repository.save(new Employee("Meriadoc", "Brandybuck", "pony rider")); - this.repository.save(new Employee("Peregrin", "Took", "pipe smoker")); - } -} -// end::code[] \ No newline at end of file diff --git a/conditional/src/main/java/com/greglturnquist/payroll/Employee.java b/conditional/src/main/java/com/greglturnquist/payroll/Employee.java deleted file mode 100644 index aedf65d..0000000 --- a/conditional/src/main/java/com/greglturnquist/payroll/Employee.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Version; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Entity -public class Employee { - - private @Id @GeneratedValue Long id; - private String firstName; - private String lastName; - private String description; - - private @Version @JsonIgnore Long version; - - private Employee() {} - - public Employee(String firstName, String lastName, String description) { - this.firstName = firstName; - this.lastName = lastName; - this.description = description; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Employee employee = (Employee) o; - return Objects.equals(id, employee.id) && - Objects.equals(firstName, employee.firstName) && - Objects.equals(lastName, employee.lastName) && - Objects.equals(description, employee.description) && - Objects.equals(version, employee.version); - } - - @Override - public int hashCode() { - - return Objects.hash(id, firstName, lastName, description, version); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - @Override - public String toString() { - return "Employee{" + - "id=" + id + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", description='" + description + '\'' + - ", version=" + version + - '}'; - } -} -// end::code[] \ No newline at end of file diff --git a/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java deleted file mode 100644 index 748125c..0000000 --- a/conditional/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.data.repository.PagingAndSortingRepository; - -/** - * @author Greg Turnquist - */ -// tag::code[] -public interface EmployeeRepository extends PagingAndSortingRepository { - -} -// end::code[] diff --git a/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java b/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java deleted file mode 100644 index abaaba2..0000000 --- a/conditional/src/main/java/com/greglturnquist/payroll/HomeController.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Controller -public class HomeController { - - @RequestMapping(value = "/") - public String index() { - return "index"; - } - -} -// end::code[] \ No newline at end of file diff --git a/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java deleted file mode 100644 index 7b2fc31..0000000 --- a/conditional/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@SpringBootApplication -public class ReactAndSpringDataRestApplication { - - public static void main(String[] args) { - SpringApplication.run(ReactAndSpringDataRestApplication.class, args); - } -} -// end::code[] \ No newline at end of file diff --git a/conditional/src/main/js/api/uriListConverter.js b/conditional/src/main/js/api/uriListConverter.js deleted file mode 100644 index 8d9dc2e..0000000 --- a/conditional/src/main/js/api/uriListConverter.js +++ /dev/null @@ -1,19 +0,0 @@ -define(function() { - 'use strict'; - - /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ - return { - read: function(str /*, opts */) { - return str.split('\n'); - }, - write: function(obj /*, opts */) { - // If this is an Array, extract the self URI and then join using a newline - if (obj instanceof Array) { - return obj.map(resource => resource._links.self.href).join('\n'); - } else { // otherwise, just return the self URI - return obj._links.self.href; - } - } - }; - -}); diff --git a/conditional/src/main/js/api/uriTemplateInterceptor.js b/conditional/src/main/js/api/uriTemplateInterceptor.js deleted file mode 100644 index c16ba33..0000000 --- a/conditional/src/main/js/api/uriTemplateInterceptor.js +++ /dev/null @@ -1,18 +0,0 @@ -define(function(require) { - 'use strict'; - - const interceptor = require('rest/interceptor'); - - return interceptor({ - request: function (request /*, config, meta */) { - /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */ - if (request.path.indexOf('{') === -1) { - return request; - } else { - request.path = request.path.split('{')[0]; - return request; - } - } - }); - -}); \ No newline at end of file diff --git a/conditional/src/main/js/app.js b/conditional/src/main/js/app.js deleted file mode 100644 index ae3b110..0000000 --- a/conditional/src/main/js/app.js +++ /dev/null @@ -1,392 +0,0 @@ -'use strict'; - -const React = require('react'); -const ReactDOM = require('react-dom'); -const when = require('when'); -const client = require('./client'); - -const follow = require('./follow'); // function to hop multiple links by "rel" - -const root = '/api'; - -class App extends React.Component { - - constructor(props) { - super(props); - this.state = {employees: [], attributes: [], pageSize: 2, links: {}}; - this.updatePageSize = this.updatePageSize.bind(this); - this.onCreate = this.onCreate.bind(this); - this.onUpdate = this.onUpdate.bind(this); - this.onDelete = this.onDelete.bind(this); - this.onNavigate = this.onNavigate.bind(this); - } - - // tag::follow-2[] - loadFromServer(pageSize) { - follow(client, root, [ // <1> - {rel: 'employees', params: {size: pageSize}}] - ).then(employeeCollection => { // <2> - return client({ - method: 'GET', - path: employeeCollection.entity._links.profile.href, - headers: {'Accept': 'application/schema+json'} - }).then(schema => { - this.schema = schema.entity; - this.links = employeeCollection.entity._links; - return employeeCollection; - }); - }).then(employeeCollection => { // <3> - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { // <4> - return when.all(employeePromises); - }).done(employees => { // <5> - this.setState({ - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: pageSize, - links: this.links - }); - }); - } - // end::follow-2[] - - // tag::create[] - onCreate(newEmployee) { - const self = this; - follow(client, root, ['employees']).then(response => { - return client({ - method: 'POST', - path: response.entity._links.self.href, - entity: newEmployee, - headers: {'Content-Type': 'application/json'} - }) - }).then(response => { - return follow(client, root, [{rel: 'employees', params: {'size': self.state.pageSize}}]); - }).done(response => { - if (typeof response.entity._links.last !== "undefined") { - this.onNavigate(response.entity._links.last.href); - } else { - this.onNavigate(response.entity._links.self.href); - } - }); - } - // end::create[] - - // tag::update[] - onUpdate(employee, updatedEmployee) { - client({ - method: 'PUT', - path: employee.entity._links.self.href, - entity: updatedEmployee, - headers: { - 'Content-Type': 'application/json', - 'If-Match': employee.headers.Etag - } - }).done(response => { - this.loadFromServer(this.state.pageSize); - }, response => { - if (response.status.code === 412) { - alert('DENIED: Unable to update ' + - employee.entity._links.self.href + '. Your copy is stale.'); - } - }); - } - // end::update[] - - // tag::delete[] - onDelete(employee) { - client({method: 'DELETE', path: employee.entity._links.self.href}).done(response => { - this.loadFromServer(this.state.pageSize); - }); - } - // end::delete[] - - // tag::navigate[] - onNavigate(navUri) { - client({ - method: 'GET', - path: navUri - }).then(employeeCollection => { - this.links = employeeCollection.entity._links; - - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { - return when.all(employeePromises); - }).done(employees => { - this.setState({ - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: this.state.pageSize, - links: this.links - }); - }); - } - // end::navigate[] - - // tag::update-page-size[] - updatePageSize(pageSize) { - if (pageSize !== this.state.pageSize) { - this.loadFromServer(pageSize); - } - } - // end::update-page-size[] - - // tag::follow-1[] - componentDidMount() { - this.loadFromServer(this.state.pageSize); - } - // end::follow-1[] - - render() { - return ( -
- - -
- ) - } -} - -// tag::create-dialog[] -class CreateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const newEmployee = {}; - this.props.attributes.forEach(attribute => { - newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onCreate(newEmployee); - this.props.attributes.forEach(attribute => { - ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs - }); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - return ( -
- Create - -
-
- X - -

Create new employee

- - - {inputs} - - -
-
-
- ) - } -} -// end::create-dialog[] - -// tag::update-dialog[] -class UpdateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const updatedEmployee = {}; - this.props.attributes.forEach(attribute => { - updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onUpdate(this.props.employee, updatedEmployee); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - - const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href; - - return ( -
- Update -
-
- X - -

Update an employee

- -
- {inputs} - -
-
-
-
- ) - } - -}; -// end::update-dialog[] - - -class EmployeeList extends React.Component { - - constructor(props) { - super(props); - this.handleNavFirst = this.handleNavFirst.bind(this); - this.handleNavPrev = this.handleNavPrev.bind(this); - this.handleNavNext = this.handleNavNext.bind(this); - this.handleNavLast = this.handleNavLast.bind(this); - this.handleInput = this.handleInput.bind(this); - } - - // tag::handle-page-size-updates[] - handleInput(e) { - e.preventDefault(); - const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value; - if (/^[0-9]+$/.test(pageSize)) { - this.props.updatePageSize(pageSize); - } else { - ReactDOM.findDOMNode(this.refs.pageSize).value = pageSize.substring(0, pageSize.length - 1); - } - } - // end::handle-page-size-updates[] - - // tag::handle-nav[] - handleNavFirst(e){ - e.preventDefault(); - this.props.onNavigate(this.props.links.first.href); - } - handleNavPrev(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.prev.href); - } - handleNavNext(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.next.href); - } - handleNavLast(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.last.href); - } - // end::handle-nav[] - // tag::employee-list-render[] - render() { - const employees = this.props.employees.map(employee => - - ); - - const navLinks = []; - if ("first" in this.props.links) { - navLinks.push(); - } - if ("prev" in this.props.links) { - navLinks.push(); - } - if ("next" in this.props.links) { - navLinks.push(); - } - if ("last" in this.props.links) { - navLinks.push(); - } - - return ( -
- - - - - - - - - - - {employees} - -
First NameLast NameDescription
-
- {navLinks} -
-
- ) - } - // end::employee-list-render[] -} - -// tag::employee[] -class Employee extends React.Component { - - constructor(props) { - super(props); - this.handleDelete = this.handleDelete.bind(this); - } - - handleDelete() { - this.props.onDelete(this.props.employee); - } - - render() { - return ( - - {this.props.employee.entity.firstName} - {this.props.employee.entity.lastName} - {this.props.employee.entity.description} - - - - - - - - ) - } -} -// end::employee[] - -ReactDOM.render( - , - document.getElementById('react') -) diff --git a/conditional/src/main/js/client.js b/conditional/src/main/js/client.js deleted file mode 100644 index dfecbea..0000000 --- a/conditional/src/main/js/client.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const rest = require('rest'); -const defaultRequest = require('rest/interceptor/defaultRequest'); -const mime = require('rest/interceptor/mime'); -const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); -const errorCode = require('rest/interceptor/errorCode'); -const baseRegistry = require('rest/mime/registry'); - -const registry = baseRegistry.child(); - -registry.register('text/uri-list', require('./api/uriListConverter')); -registry.register('application/hal+json', require('rest/mime/type/application/hal')); - -module.exports = rest - .wrap(mime, { registry: registry }) - .wrap(uriTemplateInterceptor) - .wrap(errorCode) - .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); diff --git a/conditional/src/main/js/follow.js b/conditional/src/main/js/follow.js deleted file mode 100644 index 59efe70..0000000 --- a/conditional/src/main/js/follow.js +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = function follow(api, rootPath, relArray) { - const root = api({ - method: 'GET', - path: rootPath - }); - - return relArray.reduce(function(root, arrayItem) { - const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; - return traverseNext(root, rel, arrayItem); - }, root); - - function traverseNext (root, rel, arrayItem) { - return root.then(function (response) { - if (hasEmbeddedRel(response.entity, rel)) { - return response.entity._embedded[rel]; - } - - if(!response.entity._links) { - return []; - } - - if (typeof arrayItem === 'string') { - return api({ - method: 'GET', - path: response.entity._links[rel].href - }); - } else { - return api({ - method: 'GET', - path: response.entity._links[rel].href, - params: arrayItem.params - }); - } - }); - } - - function hasEmbeddedRel (entity, rel) { - return entity._embedded && entity._embedded.hasOwnProperty(rel); - } -}; diff --git a/conditional/src/main/resources/application.properties b/conditional/src/main/resources/application.properties deleted file mode 100644 index d329a65..0000000 --- a/conditional/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.data.rest.base-path=/api \ No newline at end of file diff --git a/conditional/src/main/resources/static/main.css b/conditional/src/main/resources/static/main.css deleted file mode 100644 index c95a16d..0000000 --- a/conditional/src/main/resources/static/main.css +++ /dev/null @@ -1,69 +0,0 @@ -input.field { - width: 90%; -} - -table { - border-collapse: collapse; -} - -td, th { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; -} - -/*Classes for creating dialogs*/ - -.modalDialog { - position: fixed; - font-family: Arial, Helvetica, sans-serif; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0,0,0,0.8); - z-index: 99999; - opacity:0; - -webkit-transition: opacity 400ms ease-in; - -moz-transition: opacity 400ms ease-in; - transition: opacity 400ms ease-in; - pointer-events: none; -} - -.modalDialog:target { - opacity:1; - pointer-events: auto; -} - -.modalDialog > div { - width: 400px; - position: relative; - margin: 10% auto; - padding: 5px 20px 13px 20px; - border-radius: 10px; - background: #fff; - background: -moz-linear-gradient(#fff, #999); - background: -webkit-linear-gradient(#fff, #999); - background: -o-linear-gradient(#fff, #999); -} - -.close { - background: #606061; - color: #FFFFFF; - line-height: 25px; - position: absolute; - right: -12px; - text-align: center; - top: -10px; - width: 24px; - text-decoration: none; - font-weight: bold; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; - border-radius: 12px; - -moz-box-shadow: 1px 1px 3px #000; - -webkit-box-shadow: 1px 1px 3px #000; - box-shadow: 1px 1px 3px #000; -} - -.close:hover { background: #00d9ff; } diff --git a/conditional/src/main/resources/templates/index.html b/conditional/src/main/resources/templates/index.html deleted file mode 100644 index c5f2c1e..0000000 --- a/conditional/src/main/resources/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - ReactJS + Spring Data REST - - - - -
- - - - - \ No newline at end of file diff --git a/conditional/webpack.config.js b/conditional/webpack.config.js deleted file mode 100644 index 8d782ae..0000000 --- a/conditional/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -var path = require('path'); - -module.exports = { - entry: './src/main/js/app.js', - devtool: 'sourcemaps', - cache: true, - mode: 'development', - output: { - path: __dirname, - filename: './src/main/resources/static/built/bundle.js' - }, - module: { - rules: [ - { - test: path.join(__dirname, '.'), - exclude: /(node_modules)/, - use: [{ - loader: 'babel-loader', - options: { - presets: ["@babel/preset-env", "@babel/preset-react"] - } - }] - } - ] - } -}; \ No newline at end of file diff --git a/events/.mvn/wrapper/MavenWrapperDownloader.java b/events/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3..0000000 --- a/events/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/events/.mvn/wrapper/maven-wrapper.jar b/events/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a..0000000 Binary files a/events/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/events/.mvn/wrapper/maven-wrapper.properties b/events/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572..0000000 --- a/events/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/events/README.adoc b/events/README.adoc deleted file mode 100644 index 9a520e4..0000000 --- a/events/README.adoc +++ /dev/null @@ -1,178 +0,0 @@ -[[react-and-spring-data-rest-part-4]] -= Part 4 - Events -:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master - -In the <>, you introduced conditional updates to avoid collisions with other users when editing the same data. You also learned how to version data on the backend with optimistic locking. You got a notification if someone edited the same record so you could refresh the page and get the update. - -That is good. But do you know what is even better? Having the UI dynamically respond when other people update the resources. - -In this section, you will learn how to use Spring Data REST's built in event system to detect changes in the backend and publish updates to ALL users through Spring's WebSocket support. Then you will be able to dynamically adjust clients as the data updates. - -Feel free to {sourcedir}/events[grab the code] from this repository and follow along. This section is based on the previous section's application, with extra things added. - -== Adding Spring WebSocket Support to the Project - -Before getting underway, you need to add a dependency to your project's pom.xml file: - -==== -[source,xml,indent=0] ----- -include::pom.xml[tag=websocket] ----- -==== - -This dependency brings in Spring Boot's WebSocket starter. - -== Configuring WebSockets with Spring - -https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#websocket[Spring comes with powerful WebSocket support]. One thing to recognize is that a WebSocket is a very low-level protocol. It does little more than offer the means to transmit data between client and server. The recommendation is to use a sub-protocol (STOMP for this section) to actually encode data and routes. - -The following code configures WebSocket support on the server side: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java[tag=code] ----- - -<1> `@EnableWebSocketMessageBroker` turns on WebSocket support. -<2> `WebSocketMessageBrokerConfigurer` provides a convenient base class to configure basic features. -<3> *MESSAGE_PREFIX* is the prefix you will prepend to every message's route. -<4> `registerStompEndpoints()` is used to configure the endpoint on the backend for clients and server to link (`/payroll`). -<5> `configureMessageBroker()` is used to configure the broker used to relay messages between server and client. -==== - -With this configuration, you can now tap into Spring Data REST events and publish them over a WebSocket. - -== Subscribing to Spring Data REST Events - -Spring Data REST generates several https://docs.spring.io/spring-data/rest/docs/current/reference/html/#events[application events] based on actions occurring on the repositories. The following code shows how to subscribe to some of these events: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/EventHandler.java[tag=code] ----- - -<1> `@RepositoryEventHandler(Employee.class)` flags this class to trap events based on *employees*. -<2> `SimpMessagingTemplate` and `EntityLinks` are autowired from the application context. -<3> The `@HandleXYZ` annotations flag the methods that need to listen to events. These methods must be public. -==== - -Each of these handler methods invokes `SimpMessagingTemplate.convertAndSend()` to transmit a message over the WebSocket. This is a pub-sub approach so that one message is relayed to every attached consumer. - -The route of each message is different, allowing multiple messages to be sent to distinct receivers on the client while needing only one open WebSocket -- a resource-efficient approach. - -`getPath()` uses Spring Data REST's `EntityLinks` to look up the path for a given class type and id. To serve the client's needs, this `Link` object is converted to a Java URI with its path extracted. - -NOTE: `EntityLinks` comes with several utility methods to programmatically find the paths of various resources, whether single or for collections. - -In essence, you are listening for create, update, and delete events, and, after they are completed, sending notice of them to all clients. You can also intercept such operations BEFORE they happen, and perhaps log them, block them for some reason, or decorate the domain objects with extra information. (In the next section, we will see a handy use for this.) - -== Configuring a JavaScript WebSocket - -The next step is to write some client-side code to consume WebSocket events. The following chunk in the main application pulls in a module: - -==== -[source,javascript] ----- -var stompClient = require('./websocket-listener') ----- -==== - -That module is shown below: - -==== -[source,javascript] ----- -include::src/main/js/websocket-listener.js[] ----- - -<1> Pull in the SockJS JavaScript library for talking over WebSockets. -<2> Pull in the stomp-websocket JavaScript library to use the STOMP sub-protocol. -<3> Point the WebSocket at the application's `/payroll` endpoint. -<4> Iterate over the array of `registrations` supplied so that each can subscribe for callback as messages arrive. -==== - -Each registration entry has a `route` and a `callback`. In the next section, you can see how to register event handlers. - -== Registering for WebSocket Events - -In React, a component's `componentDidMount()` function gets called after it has been rendered in the DOM. That is also the right time to register for WebSocket events, because the component is now online and ready for business. The following code does so: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=register-handlers] ----- -==== - -The first line is the same as before, where all the employees are fetched from the server using page size. The second line shows an array of JavaScript objects being registered for WebSocket events, each with a `route` and a `callback`. - -When a new employee is created, the behavior is to refresh the data set and then use the paging links to navigate to the *last* page. Why refresh the data before navigating to the end? It is possible that adding a new record causes a new page to get created. While it is possible to calculate if this will happen, it subverts the point of hypermedia. Instead of cobbling together customized page counts, it is better to use existing links and only go down that road if there is a performance-driving reason to do so. - -When an employee is updated or deleted, the behavior is to refresh the current page. When you update a record, it impacts the page your are viewing. When you delete a record on the current page, a record from the next page will get pulled into the current one -- hence the need to also refresh the current page. - -NOTE: There is no requirement for these WebSocket messages to start with `/topic`. It is a common convention that indicates pub-sub semantics. - -In the next section, you can see the actual operations to perform these operations. - -== Reacting to WebSocket Events and Updating the UI State - -The following chunk of code contains the two callbacks used to update UI state when a WebSocket event is received: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=websocket-handlers] ----- -==== - -`refreshAndGoToLastPage()` uses the familiar `follow()` function to navigate to the `employees` link with the `size` parameter applied, plugging in `this.state.pageSize`. When the response is received, you then invoke the same `onNavigate()` function from the last section and jump to the *last* page, the one where the new record will be found. - -`refreshCurrentPage()` also uses the `follow()` function but applies `this.state.pageSize` to `size` and `this.state.page.number` to `page`. This fetches the same page you are currently looking at and updates the state accordingly. - -NOTE: This behavior tells every client to refresh their current page when an update or delete message is sent. It is possible that their current page may have nothing to do with the current event. However, it can be tricky to figure that out. What if the record that was deleted was on page two and you are looking at page three? Every entry would change. But is this desired behavior at all? Maybe. Maybe not. - -== Moving State Management Out of the Local Updates - -Before you finish this section, there is something to recognize. You just added a new way for the state in the UI to get updated: when a WebSocket message arrives. But the old way to update the state is still there. - -To simplify your code's management of state, remove the old way. In other words, submit your `POST`, `PUT`, and `DELETE` calls, but do not use their results to update the UI's state. Instead, wait for the WebSocket event to circle back and then do the update. - -The follow chunk of code shows the same `onCreate()` function as the previous section, only simplified: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=on-create] ----- -==== - -Here, the `follow()` function is used to get to the `employees` link, and then the `POST` operation is applied. Notice how `client({method: 'GET' ...})` has no `then()` or `done()`, as before? The event handler to listen for updates is now found in `refreshAndGoToLastPage()`, which you just looked at. - -== Putting It All Together - -With all these modifications in place, fire up the application (`./mvnw spring-boot:run`) and poke around with it. Open up two browser tabs and resize so you can see them both. Start making updates in one and see how they instantly update the other tab. Open up your phone and visit the same page. Find a friend and ask that person to do the same thing. You might find this type of dynamic updating more keen. - -Want a challenge? Try the exercise from the previous section where you open the same record in two different browser tabs. Try to update it in one and NOT see it update in the other. If it is possible, the conditional `PUT` code should still protect you. But it may be trickier to pull that off! - -== Review - -In this section, you: - -* Configured Spring's WebSocket support with a SockJS fallback. -* Subscribed for create, update, and delete events from Spring Data REST to dynamically update the UI. -* Published the URI of affected REST resources along with a contextual message ("/topic/newEmployee", "/topic/updateEmployee", and so on). -* Registered WebSocket listeners in the UI to listen for these events. -* Wired the listeners to handlers to update the UI state. - -With all these features, it is easy to run two browsers, side-by-side, and see how updating one ripples to the other. - -Issues? - -While multiple displays nicely update, polishing the precise behavior is warranted. For example, creating a new user will cause ALL users to jump to the end. Any thoughts on how this should be handled? - -Paging is useful, but it offers a tricky state to manage. The costs are low on this sample application, and React is very efficient at updating the DOM without causing lots of flickering in the UI. But with a more complex application, not all of these approaches will fit. - -When designing with paging in mind, you have to decide what is the expected behavior between clients and if there needs to be updates or not. Depending on your requirements and performance of the system, the existing navigational hypermedia may be sufficient. diff --git a/events/mvnw b/events/mvnw deleted file mode 100755 index a16b543..0000000 --- a/events/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/events/mvnw.cmd b/events/mvnw.cmd deleted file mode 100644 index c8d4337..0000000 --- a/events/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/events/package.json b/events/package.json deleted file mode 100644 index 977b4e9..0000000 --- a/events/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "spring-data-rest-and-reactjs", - "version": "0.1.0", - "description": "Demo of ReactJS + Spring Data REST", - "repository": { - "type": "git", - "url": "git@github.com:spring-guides/tut-react-and-spring-data-rest.git" - }, - "keywords": [ - "rest", - "hateoas", - "spring", - "data", - "react" - ], - "author": "Greg L. Turnquist", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues" - }, - "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest", - "dependencies": { - "react": "^16.5.2", - "react-dom": "^16.5.2", - "rest": "^1.3.1", - "sockjs-client": "^1.0.3", - "stompjs": "^2.3.3" - }, - "scripts": { - "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js" - }, - "devDependencies": { - "@babel/core": "^7.1.0", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "babel-loader": "^8.0.2", - "webpack": "^4.19.1", - "webpack-cli": "^3.1.0" - } -} diff --git a/events/pom.xml b/events/pom.xml deleted file mode 100644 index 21d2e5f..0000000 --- a/events/pom.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - 4.0.0 - - - com.greglturnquist - react-and-spring-data-rest - 0.0.1-SNAPSHOT - - - react-and-spring-data-rest-events - 0.0.1-SNAPSHOT - jar - - React.js and Spring Data REST - Events - An SPA with ReactJS in the frontend and Spring Data REST in the backend - - - UTF-8 - 1.8 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-data-rest - - - org.springframework.boot - spring-boot-devtools - - - - org.springframework.boot - spring-boot-starter-websocket - - - - com.h2database - h2 - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - com.github.eirslett - frontend-maven-plugin - - - - com.rimerosolutions.maven.plugins - wrapper-maven-plugin - 0.0.4 - - 3.3.3 - - - - - - - \ No newline at end of file diff --git a/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java deleted file mode 100644 index cb339c2..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -public class DatabaseLoader implements CommandLineRunner { - - private final EmployeeRepository repository; - - @Autowired - public DatabaseLoader(EmployeeRepository repository) { - this.repository = repository; - } - - @Override - public void run(String... strings) throws Exception { - - this.repository.save(new Employee("Frodo", "Baggins", "ring bearer")); - this.repository.save(new Employee("Bilbo", "Baggins", "burglar")); - this.repository.save(new Employee("Gandalf", "the Grey", "wizard")); - this.repository.save(new Employee("Samwise", "Gamgee", "gardener")); - this.repository.save(new Employee("Meriadoc", "Brandybuck", "pony rider")); - this.repository.save(new Employee("Peregrin", "Took", "pipe smoker")); - } -} -// end::code[] \ No newline at end of file diff --git a/events/src/main/java/com/greglturnquist/payroll/Employee.java b/events/src/main/java/com/greglturnquist/payroll/Employee.java deleted file mode 100644 index aedf65d..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/Employee.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.Version; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Entity -public class Employee { - - private @Id @GeneratedValue Long id; - private String firstName; - private String lastName; - private String description; - - private @Version @JsonIgnore Long version; - - private Employee() {} - - public Employee(String firstName, String lastName, String description) { - this.firstName = firstName; - this.lastName = lastName; - this.description = description; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Employee employee = (Employee) o; - return Objects.equals(id, employee.id) && - Objects.equals(firstName, employee.firstName) && - Objects.equals(lastName, employee.lastName) && - Objects.equals(description, employee.description) && - Objects.equals(version, employee.version); - } - - @Override - public int hashCode() { - - return Objects.hash(id, firstName, lastName, description, version); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - @Override - public String toString() { - return "Employee{" + - "id=" + id + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", description='" + description + '\'' + - ", version=" + version + - '}'; - } -} -// end::code[] \ No newline at end of file diff --git a/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java deleted file mode 100644 index 748125c..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.data.repository.PagingAndSortingRepository; - -/** - * @author Greg Turnquist - */ -// tag::code[] -public interface EmployeeRepository extends PagingAndSortingRepository { - -} -// end::code[] diff --git a/events/src/main/java/com/greglturnquist/payroll/EventHandler.java b/events/src/main/java/com/greglturnquist/payroll/EventHandler.java deleted file mode 100644 index 07f4bcd..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/EventHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import static com.greglturnquist.payroll.WebSocketConfiguration.*; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.core.annotation.HandleAfterCreate; -import org.springframework.data.rest.core.annotation.HandleAfterDelete; -import org.springframework.data.rest.core.annotation.HandleAfterSave; -import org.springframework.data.rest.core.annotation.RepositoryEventHandler; -import org.springframework.hateoas.server.EntityLinks; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -@RepositoryEventHandler(Employee.class) // <1> -public class EventHandler { - - private final SimpMessagingTemplate websocket; // <2> - - private final EntityLinks entityLinks; - - @Autowired - public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) { - this.websocket = websocket; - this.entityLinks = entityLinks; - } - - @HandleAfterCreate // <3> - public void newEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/newEmployee", getPath(employee)); - } - - @HandleAfterDelete // <3> - public void deleteEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/deleteEmployee", getPath(employee)); - } - - @HandleAfterSave // <3> - public void updateEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/updateEmployee", getPath(employee)); - } - - /** - * Take an {@link Employee} and get the URI using Spring Data REST's {@link EntityLinks}. - * - * @param employee - */ - private String getPath(Employee employee) { - return this.entityLinks.linkForItemResource(employee.getClass(), - employee.getId()).toUri().getPath(); - } - -} -// end::code[] diff --git a/events/src/main/java/com/greglturnquist/payroll/HomeController.java b/events/src/main/java/com/greglturnquist/payroll/HomeController.java deleted file mode 100644 index abaaba2..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/HomeController.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Controller -public class HomeController { - - @RequestMapping(value = "/") - public String index() { - return "index"; - } - -} -// end::code[] \ No newline at end of file diff --git a/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java deleted file mode 100644 index 7b2fc31..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@SpringBootApplication -public class ReactAndSpringDataRestApplication { - - public static void main(String[] args) { - SpringApplication.run(ReactAndSpringDataRestApplication.class, args); - } -} -// end::code[] \ No newline at end of file diff --git a/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java b/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java deleted file mode 100644 index 871a1ba..0000000 --- a/events/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.messaging.simp.config.MessageBrokerRegistry; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; -import org.springframework.web.socket.config.annotation.StompEndpointRegistry; -import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -@EnableWebSocketMessageBroker // <1> -public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { // <2> - - static final String MESSAGE_PREFIX = "/topic"; // <3> - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { // <4> - registry.addEndpoint("/payroll").withSockJS(); - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { // <5> - registry.enableSimpleBroker(MESSAGE_PREFIX); - registry.setApplicationDestinationPrefixes("/app"); - } -} -// end::code[] diff --git a/events/src/main/js/api/uriListConverter.js b/events/src/main/js/api/uriListConverter.js deleted file mode 100644 index 8d9dc2e..0000000 --- a/events/src/main/js/api/uriListConverter.js +++ /dev/null @@ -1,19 +0,0 @@ -define(function() { - 'use strict'; - - /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ - return { - read: function(str /*, opts */) { - return str.split('\n'); - }, - write: function(obj /*, opts */) { - // If this is an Array, extract the self URI and then join using a newline - if (obj instanceof Array) { - return obj.map(resource => resource._links.self.href).join('\n'); - } else { // otherwise, just return the self URI - return obj._links.self.href; - } - } - }; - -}); diff --git a/events/src/main/js/api/uriTemplateInterceptor.js b/events/src/main/js/api/uriTemplateInterceptor.js deleted file mode 100644 index c16ba33..0000000 --- a/events/src/main/js/api/uriTemplateInterceptor.js +++ /dev/null @@ -1,18 +0,0 @@ -define(function(require) { - 'use strict'; - - const interceptor = require('rest/interceptor'); - - return interceptor({ - request: function (request /*, config, meta */) { - /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */ - if (request.path.indexOf('{') === -1) { - return request; - } else { - request.path = request.path.split('{')[0]; - return request; - } - } - }); - -}); \ No newline at end of file diff --git a/events/src/main/js/app.js b/events/src/main/js/app.js deleted file mode 100644 index d3cafd6..0000000 --- a/events/src/main/js/app.js +++ /dev/null @@ -1,425 +0,0 @@ -'use strict'; - -const React = require('react'); -const ReactDOM = require('react-dom'); -const when = require('when'); -const client = require('./client'); - -const follow = require('./follow'); // function to hop multiple links by "rel" - -const stompClient = require('./websocket-listener'); - -const root = '/api'; - -class App extends React.Component { - - constructor(props) { - super(props); - this.state = {employees: [], attributes: [], page: 1, pageSize: 2, links: {}}; - this.updatePageSize = this.updatePageSize.bind(this); - this.onCreate = this.onCreate.bind(this); - this.onUpdate = this.onUpdate.bind(this); - this.onDelete = this.onDelete.bind(this); - this.onNavigate = this.onNavigate.bind(this); - this.refreshCurrentPage = this.refreshCurrentPage.bind(this); - this.refreshAndGoToLastPage = this.refreshAndGoToLastPage.bind(this); - } - - loadFromServer(pageSize) { - follow(client, root, [ - {rel: 'employees', params: {size: pageSize}}] - ).then(employeeCollection => { - return client({ - method: 'GET', - path: employeeCollection.entity._links.profile.href, - headers: {'Accept': 'application/schema+json'} - }).then(schema => { - this.schema = schema.entity; - this.links = employeeCollection.entity._links; - return employeeCollection; - }); - }).then(employeeCollection => { - this.page = employeeCollection.entity.page; - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { - return when.all(employeePromises); - }).done(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: pageSize, - links: this.links - }); - }); - } - - // tag::on-create[] - onCreate(newEmployee) { - follow(client, root, ['employees']).done(response => { - client({ - method: 'POST', - path: response.entity._links.self.href, - entity: newEmployee, - headers: {'Content-Type': 'application/json'} - }) - }) - } - // end::on-create[] - - onUpdate(employee, updatedEmployee) { - client({ - method: 'PUT', - path: employee.entity._links.self.href, - entity: updatedEmployee, - headers: { - 'Content-Type': 'application/json', - 'If-Match': employee.headers.Etag - } - }).done(response => { - /* Let the websocket handler update the state */ - }, response => { - if (response.status.code === 412) { - alert('DENIED: Unable to update ' + employee.entity._links.self.href + '. Your copy is stale.'); - } - }); - } - - onDelete(employee) { - client({method: 'DELETE', path: employee.entity._links.self.href}); - } - - onNavigate(navUri) { - client({ - method: 'GET', - path: navUri - }).then(employeeCollection => { - this.links = employeeCollection.entity._links; - this.page = employeeCollection.entity.page; - - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { - return when.all(employeePromises); - }).done(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: this.state.pageSize, - links: this.links - }); - }); - } - - updatePageSize(pageSize) { - if (pageSize !== this.state.pageSize) { - this.loadFromServer(pageSize); - } - } - - // tag::websocket-handlers[] - refreshAndGoToLastPage(message) { - follow(client, root, [{ - rel: 'employees', - params: {size: this.state.pageSize} - }]).done(response => { - if (response.entity._links.last !== undefined) { - this.onNavigate(response.entity._links.last.href); - } else { - this.onNavigate(response.entity._links.self.href); - } - }) - } - - refreshCurrentPage(message) { - follow(client, root, [{ - rel: 'employees', - params: { - size: this.state.pageSize, - page: this.state.page.number - } - }]).then(employeeCollection => { - this.links = employeeCollection.entity._links; - this.page = employeeCollection.entity.page; - - return employeeCollection.entity._embedded.employees.map(employee => { - return client({ - method: 'GET', - path: employee._links.self.href - }) - }); - }).then(employeePromises => { - return when.all(employeePromises); - }).then(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: this.state.pageSize, - links: this.links - }); - }); - } - // end::websocket-handlers[] - - // tag::register-handlers[] - componentDidMount() { - this.loadFromServer(this.state.pageSize); - stompClient.register([ - {route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage}, - {route: '/topic/updateEmployee', callback: this.refreshCurrentPage}, - {route: '/topic/deleteEmployee', callback: this.refreshCurrentPage} - ]); - } - // end::register-handlers[] - - render() { - return ( -
- - -
- ) - } -} - -class CreateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const newEmployee = {}; - this.props.attributes.forEach(attribute => { - newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onCreate(newEmployee); - this.props.attributes.forEach(attribute => { - ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs - }); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - return ( -
- Create - -
-
- X - -

Create new employee

- -
- {inputs} - -
-
-
-
- ) - } -} - -class UpdateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const updatedEmployee = {}; - this.props.attributes.forEach(attribute => { - updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onUpdate(this.props.employee, updatedEmployee); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - - const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href; - - return ( -
- Update - -
-
- X - -

Update an employee

- -
- {inputs} - -
-
-
-
- ) - } - -} - -class EmployeeList extends React.Component { - - constructor(props) { - super(props); - this.handleNavFirst = this.handleNavFirst.bind(this); - this.handleNavPrev = this.handleNavPrev.bind(this); - this.handleNavNext = this.handleNavNext.bind(this); - this.handleNavLast = this.handleNavLast.bind(this); - this.handleInput = this.handleInput.bind(this); - } - - handleInput(e) { - e.preventDefault(); - const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value; - if (/^[0-9]+$/.test(pageSize)) { - this.props.updatePageSize(pageSize); - } else { - ReactDOM.findDOMNode(this.refs.pageSize).value = pageSize.substring(0, pageSize.length - 1); - } - } - - handleNavFirst(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.first.href); - } - - handleNavPrev(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.prev.href); - } - - handleNavNext(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.next.href); - } - - handleNavLast(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.last.href); - } - - render() { - const pageInfo = this.props.page.hasOwnProperty("number") ? -

Employees - Page {this.props.page.number + 1} of {this.props.page.totalPages}

: null; - - const employees = this.props.employees.map(employee => - - ); - - const navLinks = []; - if ("first" in this.props.links) { - navLinks.push(); - } - if ("prev" in this.props.links) { - navLinks.push(); - } - if ("next" in this.props.links) { - navLinks.push(); - } - if ("last" in this.props.links) { - navLinks.push(); - } - - return ( -
- {pageInfo} - - - - - - - - - - - {employees} - -
First NameLast NameDescription
-
- {navLinks} -
-
- ) - } -} - -class Employee extends React.Component { - - constructor(props) { - super(props); - this.handleDelete = this.handleDelete.bind(this); - } - - handleDelete() { - this.props.onDelete(this.props.employee); - } - - render() { - return ( - - {this.props.employee.entity.firstName} - {this.props.employee.entity.lastName} - {this.props.employee.entity.description} - - - - - - - - ) - } -} - -ReactDOM.render( - , - document.getElementById('react') -) diff --git a/events/src/main/js/client.js b/events/src/main/js/client.js deleted file mode 100644 index dfecbea..0000000 --- a/events/src/main/js/client.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const rest = require('rest'); -const defaultRequest = require('rest/interceptor/defaultRequest'); -const mime = require('rest/interceptor/mime'); -const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); -const errorCode = require('rest/interceptor/errorCode'); -const baseRegistry = require('rest/mime/registry'); - -const registry = baseRegistry.child(); - -registry.register('text/uri-list', require('./api/uriListConverter')); -registry.register('application/hal+json', require('rest/mime/type/application/hal')); - -module.exports = rest - .wrap(mime, { registry: registry }) - .wrap(uriTemplateInterceptor) - .wrap(errorCode) - .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); diff --git a/events/src/main/js/follow.js b/events/src/main/js/follow.js deleted file mode 100644 index 59efe70..0000000 --- a/events/src/main/js/follow.js +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = function follow(api, rootPath, relArray) { - const root = api({ - method: 'GET', - path: rootPath - }); - - return relArray.reduce(function(root, arrayItem) { - const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; - return traverseNext(root, rel, arrayItem); - }, root); - - function traverseNext (root, rel, arrayItem) { - return root.then(function (response) { - if (hasEmbeddedRel(response.entity, rel)) { - return response.entity._embedded[rel]; - } - - if(!response.entity._links) { - return []; - } - - if (typeof arrayItem === 'string') { - return api({ - method: 'GET', - path: response.entity._links[rel].href - }); - } else { - return api({ - method: 'GET', - path: response.entity._links[rel].href, - params: arrayItem.params - }); - } - }); - } - - function hasEmbeddedRel (entity, rel) { - return entity._embedded && entity._embedded.hasOwnProperty(rel); - } -}; diff --git a/events/src/main/js/websocket-listener.js b/events/src/main/js/websocket-listener.js deleted file mode 100644 index c87a1c5..0000000 --- a/events/src/main/js/websocket-listener.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const SockJS = require('sockjs-client'); // <1> -require('stompjs'); // <2> - -function register(registrations) { - const socket = SockJS('/payroll'); // <3> - const stompClient = Stomp.over(socket); - stompClient.connect({}, function(frame) { - registrations.forEach(function (registration) { // <4> - stompClient.subscribe(registration.route, registration.callback); - }); - }); -} - -module.exports.register = register; - diff --git a/events/src/main/resources/application.properties b/events/src/main/resources/application.properties deleted file mode 100644 index d329a65..0000000 --- a/events/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.data.rest.base-path=/api \ No newline at end of file diff --git a/events/src/main/resources/static/main.css b/events/src/main/resources/static/main.css deleted file mode 100644 index c95a16d..0000000 --- a/events/src/main/resources/static/main.css +++ /dev/null @@ -1,69 +0,0 @@ -input.field { - width: 90%; -} - -table { - border-collapse: collapse; -} - -td, th { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; -} - -/*Classes for creating dialogs*/ - -.modalDialog { - position: fixed; - font-family: Arial, Helvetica, sans-serif; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0,0,0,0.8); - z-index: 99999; - opacity:0; - -webkit-transition: opacity 400ms ease-in; - -moz-transition: opacity 400ms ease-in; - transition: opacity 400ms ease-in; - pointer-events: none; -} - -.modalDialog:target { - opacity:1; - pointer-events: auto; -} - -.modalDialog > div { - width: 400px; - position: relative; - margin: 10% auto; - padding: 5px 20px 13px 20px; - border-radius: 10px; - background: #fff; - background: -moz-linear-gradient(#fff, #999); - background: -webkit-linear-gradient(#fff, #999); - background: -o-linear-gradient(#fff, #999); -} - -.close { - background: #606061; - color: #FFFFFF; - line-height: 25px; - position: absolute; - right: -12px; - text-align: center; - top: -10px; - width: 24px; - text-decoration: none; - font-weight: bold; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; - border-radius: 12px; - -moz-box-shadow: 1px 1px 3px #000; - -webkit-box-shadow: 1px 1px 3px #000; - box-shadow: 1px 1px 3px #000; -} - -.close:hover { background: #00d9ff; } diff --git a/events/src/main/resources/templates/index.html b/events/src/main/resources/templates/index.html deleted file mode 100644 index c5f2c1e..0000000 --- a/events/src/main/resources/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - ReactJS + Spring Data REST - - - - -
- - - - - \ No newline at end of file diff --git a/events/webpack.config.js b/events/webpack.config.js deleted file mode 100644 index 8f4b525..0000000 --- a/events/webpack.config.js +++ /dev/null @@ -1,31 +0,0 @@ -var path = require('path'); - -module.exports = { - entry: './src/main/js/app.js', - devtool: 'sourcemaps', - cache: true, - mode: 'development', - resolve: { - alias: { - 'stompjs': __dirname + '/node_modules' + '/stompjs/lib/stomp.js', - } - }, - output: { - path: __dirname, - filename: './src/main/resources/static/built/bundle.js' - }, - module: { - rules: [ - { - test: path.join(__dirname, '.'), - exclude: /(node_modules)/, - use: [{ - loader: 'babel-loader', - options: { - presets: ["@babel/preset-env", "@babel/preset-react"] - } - }] - } - ] - } -}; \ No newline at end of file diff --git a/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java b/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3..0000000 --- a/hypermedia/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/hypermedia/.mvn/wrapper/maven-wrapper.jar b/hypermedia/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a..0000000 Binary files a/hypermedia/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/hypermedia/.mvn/wrapper/maven-wrapper.properties b/hypermedia/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572..0000000 --- a/hypermedia/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/hypermedia/README.adoc b/hypermedia/README.adoc deleted file mode 100644 index ce29225..0000000 --- a/hypermedia/README.adoc +++ /dev/null @@ -1,422 +0,0 @@ -[[react-and-spring-data-rest-part-2]] -= Part 2 - Hypermedia Controls -:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master - -In the <>, you found out how to create a backend payroll service to store employee data by using Spring Data REST. A key feature it lacked was using the hypermedia controls and navigation by links. Instead, it hard-coded the path to find data. - -Feel free to {sourcedir}/hypermedia[grab the code] from this repository and follow along. This section is based on the previous section's application, with extra things added. - - -== In the Beginning, There Was Data...and Then There Was REST - -[quote, Roy T. Fielding, https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven] -I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC....What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed? - -So, what exactly ARE hypermedia controls (that is, hypertext) and how can you use them? To find out, we take a step back and look at the core mission of REST. - -The concept of REST was to borrow ideas that made the web so successful and apply them to APIs. Despite the web's vast size, dynamic nature, and low rate at which clients (that is, browsers) are updated, the web is an amazing success. Roy Fielding sought to use some of its constraints and features and see if that would afford similar expansion of API production and consumption. - -One of the constraints is to limit the number of verbs. For REST, the primary ones are GET, POST, PUT, DELETE, and PATCH. There are others, but we will not get into them here. - -* GET: Fetches the state of a resource without altering the system -* POST: Creates a new resource without saying where -* PUT: Replaces an existing resource, overwriting whatever else (if anything) is already there -* DELETE: Removes an existing resource -* PATCH: Alters an existing resource (partially rather than creating a new resource) - -These are standardized HTTP verbs with well known specifications. By picking up and using already coined HTTP operations, we need not invent a new language and educate the industry. - -Another constraint of REST is to use media types to define the format of data. Instead of everyone writing their own dialect for the exchange of information, it would be prudent to develop some media types. One of the most popular ones to be accepted is HAL, media type `application/hal+json`. It is Spring Data REST's default media type. A key value is that there is no centralized, single media type for REST. Instead, people can develop media types and plug them in and try them out. As different needs become available, the industry can flexibly move. - -A key feature of REST is to include links to relevant resources. For example, if you were looking at an order, a RESTful API would include a link to the related customer, links to the catalog of items, and perhaps a link to the store from which the order was placed. In this section, you will introduce paging and see how to also use navigational paging links. - -== Turning on Paging from the Backend - -To get underway with using frontend hypermedia controls, you need to turn on some extra controls. Spring Data REST provides paging support. To use it, tweak the repository definition as follows: - -.src/main/java/com/greglturnquist/payroll/EmployeeRepository.java -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/EmployeeRepository.java[tag=code] ----- -==== - -Your interface now extends `PagingAndSortingRepository`, which adds extra options to set page size and adds navigational links to hop from page to page. The rest of the backend is the same (except for some https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[extra pre-loaded data] to make things interesting). - -Restart the application (`./mvnw spring-boot:run`) and see how it works. Then run the following command (shown with its output) to see the paging in action: - -==== ----- -$ curl "localhost:8080/api/employees?size=2" -{ - "_links" : { - "first" : { - "href" : "http://localhost:8080/api/employees?page=0&size=2" - }, - "self" : { - "href" : "http://localhost:8080/api/employees" - }, - "next" : { - "href" : "http://localhost:8080/api/employees?page=1&size=2" - }, - "last" : { - "href" : "http://localhost:8080/api/employees?page=2&size=2" - } - }, - "_embedded" : { - "employees" : [ { - "firstName" : "Frodo", - "lastName" : "Baggins", - "description" : "ring bearer", - "_links" : { - "self" : { - "href" : "http://localhost:8080/api/employees/1" - } - } - }, { - "firstName" : "Bilbo", - "lastName" : "Baggins", - "description" : "burglar", - "_links" : { - "self" : { - "href" : "http://localhost:8080/api/employees/2" - } - } - } ] - }, - "page" : { - "size" : 2, - "totalElements" : 6, - "totalPages" : 3, - "number" : 0 - } -} ----- -==== - -The default page size is 20, but we do not have that much data. So, to see it in action, we set `?size=2`. As expected, only two employees are listed. In addition, there are also `first`, `next`, and `last` links. There is also the `self` link, which is free of context, _including page parameters_. - -If you navigate to the `next` link, you'll see a `prev` link as well. The following command (shown with its output) does so: - -==== ----- -$ curl "http://localhost:8080/api/employees?page=1&size=2" -{ - "_links" : { - "first" : { - "href" : "http://localhost:8080/api/employees?page=0&size=2" - }, - "prev" : { - "href" : "http://localhost:8080/api/employees?page=0&size=2" - }, - "self" : { - "href" : "http://localhost:8080/api/employees" - }, - "next" : { - "href" : "http://localhost:8080/api/employees?page=2&size=2" - }, - "last" : { - "href" : "http://localhost:8080/api/employees?page=2&size=2" - } - }, -... ----- -==== - -NOTE: When using `&` in URL query parameters, the command line thinks it is a line break. Wrap the whole URL with quotation marks to avoid that problem. - -That looks neat, but it will be even better when you update the frontend to take advantage of it. - -== Navigating by Relationship - -No more changes are needed on the backend to start using the hypermedia controls Spring Data REST provides out of the box. You can switch to working on the frontend. (That is part of the beauty of Spring Data REST: No messy controller updates!) - -NOTE: It is important to point out that this application is not "`Spring Data REST-specific.`" Instead, it uses http://stateless.co/hal_specification.html[HAL], https://tools.ietf.org/html/rfc6570[URI Templates], and other standards. That is why using rest.js is a snap: That library comes with HAL support. - -In the previous section, you hardcoded the path to `/api/employees`. Instead, the ONLY path you should hardcode is the root, as follows - -==== -[source,javascript] ----- -... -var root = '/api'; -... ----- -==== - -With a handy little https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/js/follow.js[`follow()` function], you can now start from the root and navigate to where you want, as follows: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=follow-1] ----- -==== - -In the previous section, the loading was done directly inside `componentDidMount()`. In this section, we are making it possible to reload the entire list of employees when the page size is updated. To do so, we have moved things into `loadFromServer()`, as follows: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=follow-2] ----- -==== - -`loadFromServer` is very similar to the previous section. However, it uses `follow()`: - -* The first argument to the `follow()` function is the `client` object used to make REST calls. -* The second argument is the root URI to start from. -* The third argument is an array of relationships to navigate along. Each one can be a string or an object. - -The array of relationships can be as simple as `["employees"]`, meaning when the first call is made, look in `_links` for the relationship (or `rel`) named `employees`. Find its `href` and navigate to it. If there is another relationship in the array, repeat the process. - -Sometimes, a `rel` by itself is not enough. In this fragment of code, it also plugs in a query parameter of `?size=`. There are other options that can be supplied, as you will see later. - -== Grabbing JSON Schema Metadata - -After navigating to `employees` with the size-based query, the `employeeCollection` is available. In the previous section, we displayed that data inside ``. In this section, you are performing another call to grab some https://json-schema.org/[JSON Schema metadata] found at `/api/profile/employees/`. - -You can see the data yourself by running the following `curl` command (shown with its output): - -==== ----- -$ curl http://localhost:8080/api/profile/employees -H "Accept:application/schema+json" -{ - "title" : "Employee", - "properties" : { - "firstName" : { - "title" : "First name", - "readOnly" : false, - "type" : "string" - }, - "lastName" : { - "title" : "Last name", - "readOnly" : false, - "type" : "string" - }, - "description" : { - "title" : "Description", - "readOnly" : false, - "type" : "string" - } - }, - "definitions" : { }, - "type" : "object", - "$schema" : "https://json-schema.org/draft-04/schema#" -} ----- -==== - -NOTE: The default form of metadata at `/profile/employees` is http://alps.io[ALPS]. In this case, though, you are using content negotiation to fetch JSON Schema. - -By capturing this information in the`` component's state, you can make good use of it later when building input forms. - -[[creating-new-records]] -== Creating New Records - -Equipped with this metadata, you can now add some extra controls to the UI. You can start by creating a new React component ``, as follows: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=create-dialog] ----- -==== - -This new component has both a `handleSubmit()` function and the expected `render()` function. - -We dig into these functions in reverse order, looking first at the `render()` function. - -[[hypermedia-rendering]] -=== Rendering - -Your code maps over the JSON Schema data found in the `attributes` property and converts it into an array of `

` elements. - -* `key` is again needed by React to distinguish between multiple child nodes. -* It is a simple text-based entry field. -* `placeholder` lets us show the user with field is which. -* You may be used to having a `name` attribute, but it is not necessary. With React, `ref` is the mechanism for grabbing a particular DOM node (as you will soon see). - -This represents the dynamic nature of the component, driven by loading data from the server. - -Inside this component's top-level `
` is an anchor tag and another `
`. The anchor tag is the button to open the dialog. And the nested `
` is the hidden dialog itself. In this example, you are using pure HTML5 and CSS3. No JavaScript at all! You can https://github.com/spring-guides/tut-react-and-spring-data-rest/blob/master/hypermedia/src/main/resources/static/main.css[see the CSS code] used to show and hide the dialog. We will not dive into that here. - -Nestled inside `
` is a form where your dynamic list of input fields are injected followed by the *Create* button. That button has an `onClick={this.handleSubmit}` event handler. This is the React way of registering an event handler. - -NOTE: React does not create event handlers on every DOM element. Instead, it has a https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation[much more performant and sophisticated] solution. You need not manage that infrastructure and can instead focus on writing functional code. - -=== Handling User Input - -The `handleSubmit()` function first stops the event from bubbling further up the hierarchy. It then uses the same JSON Schema attribute property to find each ``, by using `React.findDOMNode(this.refs[attribute])`. - -`this.refs` is a way to reach out and grab a particular React component by name. Note that you are getting ONLY the virtual DOM component. To grab the actual DOM element, you need to use `React.findDOMNode()`. - -After iterating over every input and building up the `newEmployee` object, we invoke a callback to `onCreate()` for the new employee record. This function is inside `App.onCreate` and was provided to this React component as another property. Look at how that top-level function operates: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=create] ----- -==== - -Once again, we use the `follow()` function to navigate to the `employees` resource where POST operations are performed. In this case, there was no need to apply any parameters, so the string-based array of `rel` instance is fine. In this situation, the `POST` call is returned. This allows the next `then()` clause to handle processing the outcome of the `POST`. - -New records are typically added to the end of the dataset. Since you are looking at a certain page, it is logical to expect the new employee record to not be on the current page. To handle this, you need to fetch a new batch of data with the same page size applied. That promise is returned for the final clause inside `done()`. - -Since the user probably wants to see the newly created employee, you can then use the hypermedia controls and navigate to the `last` entry. - -[[NOTE]] -===== -First time using a promise-based API? https://promisesaplus.com/[Promises] are a way to kick off asynchronous operations and then register a function to respond when the task is done. Promises are designed to be chained together to avoid "`callback hell`". Look at the following flow: - -==== -[source,javascript] ----- -when.promise(async_func_call()) - .then(function(results) { - /* process the outcome of async_func_call */ - }) - .then(function(more_results) { - /* process the previous then() return value */ - }) - .done(function(yet_more) { - /* process the previous then() and wrap things up */ - }); ----- -==== - -For more details, check out http://know.cujojs.com/tutorials/promises/consuming-promises[this tutorial on promises]. - -The secret thing to remember with promises is that `then()` functions _need_ to return something, whether it is a value or another promise. `done()` functions do NOT return anything, and you do not chain anything after one. In case you have not yet noticed, `client` (which is an instance of `rest` from rest.js) and the `follow` function return promises. -===== - -== Paging Through Data - -You have set up paging on the backend and have already starting taking advantage of it when creating new employees. - -In <>, you used the page controls to jump to the `last` page. It would be really handy to dynamically apply it to the UI and let the user navigate as desired. Adjusting the controls dynamically, based on available navigation links, would be great. - -First, let's check out the `onNavigate()` function you used: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=navigate] ----- -==== - -This is defined at the top, inside `App.onNavigate`. Again, this is to allow managing the state of the UI in the top component. After passing `onNavigate()` down to the `` React component, the following handlers are coded up to handle clicking on some buttons: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=handle-nav] ----- -==== - -Each of these functions intercepts the default event and stops it from bubbling up. Then it invokes the `onNavigate()` function with the proper hypermedia link. - -Now you can conditionally display the controls based on which links appear in the hypermedia links in `EmployeeList.render`: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=employee-list-render] ----- -==== - -As in the previous section, it still transforms `this.props.employees` into an array of `` components. Then it builds up an array of `navLinks` as an array of HTML buttons. - -NOTE: Because React is based on XML, you cannot put `<` inside the ` - -
-
-
- ) - } - -} -// end::create-dialog[] - -class EmployeeList extends React.Component { - - constructor(props) { - super(props); - this.handleNavFirst = this.handleNavFirst.bind(this); - this.handleNavPrev = this.handleNavPrev.bind(this); - this.handleNavNext = this.handleNavNext.bind(this); - this.handleNavLast = this.handleNavLast.bind(this); - this.handleInput = this.handleInput.bind(this); - } - - // tag::handle-page-size-updates[] - handleInput(e) { - e.preventDefault(); - const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value; - if (/^[0-9]+$/.test(pageSize)) { - this.props.updatePageSize(pageSize); - } else { - ReactDOM.findDOMNode(this.refs.pageSize).value = - pageSize.substring(0, pageSize.length - 1); - } - } - // end::handle-page-size-updates[] - - // tag::handle-nav[] - handleNavFirst(e){ - e.preventDefault(); - this.props.onNavigate(this.props.links.first.href); - } - - handleNavPrev(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.prev.href); - } - - handleNavNext(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.next.href); - } - - handleNavLast(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.last.href); - } - // end::handle-nav[] - - // tag::employee-list-render[] - render() { - const employees = this.props.employees.map(employee => - - ); - - const navLinks = []; - if ("first" in this.props.links) { - navLinks.push(); - } - if ("prev" in this.props.links) { - navLinks.push(); - } - if ("next" in this.props.links) { - navLinks.push(); - } - if ("last" in this.props.links) { - navLinks.push(); - } - - return ( -
- - - - - - - - - - {employees} - -
First NameLast NameDescription
-
- {navLinks} -
-
- ) - } - // end::employee-list-render[] -} - -// tag::employee[] -class Employee extends React.Component { - - constructor(props) { - super(props); - this.handleDelete = this.handleDelete.bind(this); - } - - handleDelete() { - this.props.onDelete(this.props.employee); - } - - render() { - return ( - - {this.props.employee.firstName} - {this.props.employee.lastName} - {this.props.employee.description} - - - - - ) - } -} -// end::employee[] - -ReactDOM.render( - , - document.getElementById('react') -) diff --git a/hypermedia/src/main/js/client.js b/hypermedia/src/main/js/client.js deleted file mode 100644 index dfecbea..0000000 --- a/hypermedia/src/main/js/client.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const rest = require('rest'); -const defaultRequest = require('rest/interceptor/defaultRequest'); -const mime = require('rest/interceptor/mime'); -const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); -const errorCode = require('rest/interceptor/errorCode'); -const baseRegistry = require('rest/mime/registry'); - -const registry = baseRegistry.child(); - -registry.register('text/uri-list', require('./api/uriListConverter')); -registry.register('application/hal+json', require('rest/mime/type/application/hal')); - -module.exports = rest - .wrap(mime, { registry: registry }) - .wrap(uriTemplateInterceptor) - .wrap(errorCode) - .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); diff --git a/hypermedia/src/main/js/follow.js b/hypermedia/src/main/js/follow.js deleted file mode 100644 index 59efe70..0000000 --- a/hypermedia/src/main/js/follow.js +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = function follow(api, rootPath, relArray) { - const root = api({ - method: 'GET', - path: rootPath - }); - - return relArray.reduce(function(root, arrayItem) { - const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; - return traverseNext(root, rel, arrayItem); - }, root); - - function traverseNext (root, rel, arrayItem) { - return root.then(function (response) { - if (hasEmbeddedRel(response.entity, rel)) { - return response.entity._embedded[rel]; - } - - if(!response.entity._links) { - return []; - } - - if (typeof arrayItem === 'string') { - return api({ - method: 'GET', - path: response.entity._links[rel].href - }); - } else { - return api({ - method: 'GET', - path: response.entity._links[rel].href, - params: arrayItem.params - }); - } - }); - } - - function hasEmbeddedRel (entity, rel) { - return entity._embedded && entity._embedded.hasOwnProperty(rel); - } -}; diff --git a/hypermedia/src/main/resources/application.properties b/hypermedia/src/main/resources/application.properties deleted file mode 100644 index d329a65..0000000 --- a/hypermedia/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.data.rest.base-path=/api \ No newline at end of file diff --git a/hypermedia/src/main/resources/static/main.css b/hypermedia/src/main/resources/static/main.css deleted file mode 100644 index c95a16d..0000000 --- a/hypermedia/src/main/resources/static/main.css +++ /dev/null @@ -1,69 +0,0 @@ -input.field { - width: 90%; -} - -table { - border-collapse: collapse; -} - -td, th { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; -} - -/*Classes for creating dialogs*/ - -.modalDialog { - position: fixed; - font-family: Arial, Helvetica, sans-serif; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0,0,0,0.8); - z-index: 99999; - opacity:0; - -webkit-transition: opacity 400ms ease-in; - -moz-transition: opacity 400ms ease-in; - transition: opacity 400ms ease-in; - pointer-events: none; -} - -.modalDialog:target { - opacity:1; - pointer-events: auto; -} - -.modalDialog > div { - width: 400px; - position: relative; - margin: 10% auto; - padding: 5px 20px 13px 20px; - border-radius: 10px; - background: #fff; - background: -moz-linear-gradient(#fff, #999); - background: -webkit-linear-gradient(#fff, #999); - background: -o-linear-gradient(#fff, #999); -} - -.close { - background: #606061; - color: #FFFFFF; - line-height: 25px; - position: absolute; - right: -12px; - text-align: center; - top: -10px; - width: 24px; - text-decoration: none; - font-weight: bold; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; - border-radius: 12px; - -moz-box-shadow: 1px 1px 3px #000; - -webkit-box-shadow: 1px 1px 3px #000; - box-shadow: 1px 1px 3px #000; -} - -.close:hover { background: #00d9ff; } diff --git a/hypermedia/src/main/resources/templates/index.html b/hypermedia/src/main/resources/templates/index.html deleted file mode 100644 index c5f2c1e..0000000 --- a/hypermedia/src/main/resources/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - ReactJS + Spring Data REST - - - - -
- - - - - \ No newline at end of file diff --git a/hypermedia/webpack.config.js b/hypermedia/webpack.config.js deleted file mode 100644 index 8d782ae..0000000 --- a/hypermedia/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -var path = require('path'); - -module.exports = { - entry: './src/main/js/app.js', - devtool: 'sourcemaps', - cache: true, - mode: 'development', - output: { - path: __dirname, - filename: './src/main/resources/static/built/bundle.js' - }, - module: { - rules: [ - { - test: path.join(__dirname, '.'), - exclude: /(node_modules)/, - use: [{ - loader: 'babel-loader', - options: { - presets: ["@babel/preset-env", "@babel/preset-react"] - } - }] - } - ] - } -}; \ No newline at end of file diff --git a/security/.mvn/wrapper/MavenWrapperDownloader.java b/security/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f3..0000000 --- a/security/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/security/.mvn/wrapper/maven-wrapper.jar b/security/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a..0000000 Binary files a/security/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/security/.mvn/wrapper/maven-wrapper.properties b/security/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572..0000000 --- a/security/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/security/README.adoc b/security/README.adoc deleted file mode 100644 index f5c38cb..0000000 --- a/security/README.adoc +++ /dev/null @@ -1,327 +0,0 @@ -[[react-and-spring-data-rest-part-5]] -= Part 5 - Securing the UI and the API -:sourcedir: https://github.com/spring-guides/tut-react-and-spring-data-rest/tree/master - -In the <>, you made the app dynamically response to updates from other users with Spring Data REST's built-in event handlers and the Spring Framework's WebSocket support. However, no application is complete without securing the whole thing so that only proper users have access to the UI and the resources behind it. - -Feel free to {sourcedir}/security[grab the code] from this repository and follow along. This section is based on the previous section's app with extra things added. - -== Adding Spring Security to the Project - -Before getting underway, you need to add a couple dependencies to your project's pom.xml file: - -==== -[source,xml,indent=0] ----- -include::pom.xml[tag=security] ----- -==== - -This bring in Spring Boot's Spring Security starter as well as some extra Thymeleaf tags to do security lookups in the web page. - -== Defining the Security Model - -In the past section, you have worked with a nice payroll system. It is handy to declare things on the backend and let Spring Data REST do the heavy lifting. The next step is to model a system where security controls need to be instituted. - -If this is a payroll system, then only managers would be accessing it. So kick things off by modeling a `Manager` object: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/Manager.java[tag=code] ----- - -<1> `PASSWORD_ENCODER` is the means to encrypt new passwords or to take password inputs and encrypt them before comparison. -<2> `id`, `name`, `password`, and `roles` define the parameters needed to restrict access. -<3> The customized `setPassword()` method ensures that passwords are never stored in the clear. -==== - -There is a key thing to keep in mind when designing your security layer. Secure the right bits of data (like passwords) and do NOT let them get printed to console, into logs, or exported through JSON serialization. - -* `@JsonIgnore` applied to the password field protects from Jackson serializing this field. - -== Creating a Manager's Repository - -Spring Data is so good at managing entities. Why not create a repository to handle these managers? The following code does so: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/ManagerRepository.java[tag=code] ----- -==== - -Instead of extending the usual `CrudRepository`, you do not need so many methods. Instead, you need to save data (which is also used for updates), and you need to look up existing users. Hence, you can use Spring Data Common's minimal `Repository` marker interface. It comes with no predefined operations. - -Spring Data REST, by default, will export any repository it finds. You do NOT want this repository exposed for REST operations! Apply the `@RepositoryRestResource(exported = false)` annotation to block it from export. This prevents the repository and its metadata from being served up. - -== Linking Employees with Their Managers - -The last bit of modeling security is to associate employees with a manager. In this domain, an employee can have one manager while a manager can have multiple employees. The following code defines that relationship: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/Employee.java[tag=code] ----- - -<1> The manager attribute is linked by JPA's `@ManyToOne` attribute. `Manager` does not need the `@OneToMany`, because you have not defined the need to look that up. -<2> The utility constructor call is updated to support initialization. -==== - -== Securing Employees to Their Managers - -Spring Security supports a multitude of options when it comes to defining security policies. In this section, you want to restrict things such that ONLY managers can view employee payroll data, and that saving, updating, and deleting operations are confined to the employee's manager. In other words, any manager can log in and view the data, but only a given employee's manager can make any changes. The following code achieves these goals: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/EmployeeRepository.java[tag=code] ----- - -<1> `@PreAuthorize` at the top of the interface restricts access to people with `ROLE_MANAGER`. -==== - -On `save()`, either the employee's manager is null (initial creation of a new employee when no manager has been assigned), or the employee's manager's name matches the currently authenticated user's name. Here, you are using https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#el-access[Spring Security's SpEL expressions] to define access. It comes with a handy `?.` property navigator to handle null checks. It is also important to note using the `@Param(...)` on the arguments to link HTTP operations with the methods. - -On `delete()`, the method either has access to the employee, or if it has only an `id`, it must find the `employeeRepository` in the application context, perform a `findOne(id)`, and check the manager against the currently authenticated user. - -== Writing a `UserDetails` Service - -A common point of integration with security is to define a `UserDetailsService`. This is the way to connect your user's data store into a Spring Security interface. Spring Security needs a way to look up users for security checks, and this is the bridge. Thankfully, with Spring Data, the effort is quite minimal: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/SpringDataJpaUserDetailsService.java[tag=code] ----- -==== - -`SpringDataJpaUserDetailsService` implements Spring Security's `UserDetailsService`. The interface has one method: `loadUserByUsername()`. This method is meant to return a `UserDetails` object so that Spring Security can interrogate the user's information. - -Because you have a `ManagerRepository`, there is no need to write any SQL or JPA expressions to fetch this needed data. In this class, it is autowired by constructor injection. - -`loadUserByUsername()` taps into the custom finder you wrote a moment ago, `findByName()`. It then populates a Spring Security `User` instance, which implements the `UserDetails` interface. You are also using Spring Securiy's `AuthorityUtils` to transition from an array of string-based roles into a Java `List` of type `GrantedAuthority`. - -== Wiring up Your Security Policy - -The `@PreAuthorize` expressions applied to your repository are *access rules*. These rules are for nought without a security policy: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/SecurityConfiguration.java[tag=code] ----- -==== - -This code has a lot of complexity in it, so we will walk through it, first talking about the annotations and APIs. Then we will discuss the security policy it defines. - -<1> `@EnableWebSecurity` tells Spring Boot to drop its autoconfigured security policy and use this one instead. For quick demos, autoconfigured security is okay. But for anything real, you should write the policy yourself. -<2> `@EnableGlobalMethodSecurity` turns on method-level security with Spring Security's sophisticated https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#el-pre-post-annotations[`@Pre` and `@Post` annotations]. -<3> It extends `WebSecurityConfigurerAdapter`, a handy base class for writing policy. -<4> It autowires the `SpringDataJpaUserDetailsService` by field injection and then plugs it in through the `configure(AuthenticationManagerBuilder)` method. The `PASSWORD_ENCODER` from `Manager` is also set up. -<5> The pivotal security policy is written in pure Java with the `configure(HttpSecurity)` method call. - -The security policy says to authorize all requests by using the access rules defined earlier: - -* The paths listed in `antMatchers()` are granted unconditional access, since there is no reason to block static web resources. -* Anything that does not match that policy falls into `anyRequest().authenticated()`, meaning it requires authentication. -* With those access rules set up, Spring Security is told to use form-based authentication (defaulting to `/` upon success) and to grant access to the login page. -* BASIC login is also configured with CSRF disabled. This is mostly for demonstrations and not recommended for production systems without careful analysis. -* Logout is configured to take the user to `/`. - -WARNING: BASIC authentication is handy when you are experimenting with curl. Using curl to access a form-based system is daunting. It is important to recognize that authenticating with any mechanism over HTTP (not HTTPS) puts you at risk of credentials being sniffed over the wire. CSRF is a good protocol to leave intact. It is disabled to make interaction with BASIC and curl easier. In production, it is best to leave it on. - -== Adding Security Details Automatically - -One part of a good user experience is when the application can automatically apply context. In this example, if a logged-in manager creates a new employee record, it makes sense for that manager to own it. With Spring Data REST's event handlers, there is no need for the user to explicitly link it. It also ensures the user does not accidentally assign records to the wrong manager. The `SpringDataRestEventHandler` handles that for us: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/SpringDataRestEventHandler.java[tag=code] ----- - -<1> `@RepositoryEventHandler(Employee.class)` flags this event handler as applying only to `Employee` objects. The `@HandleBeforeCreate` annotation gives you a chance to alter the incoming `Employee` record before it gets written to the database. -==== - -In this situation, you can look up the current user's security context to get the user's name. Then you can look up the associated manager by using `findByName()` and apply it to the manager. There is a little extra glue code to create a new manager if that person does not exist in the system yet. However, that is mostly to support initialization of the database. In a real production system, that code should be removed and instead depend on the DBAs or Security Ops team to properly maintain the user data store. - -== Pre-loading Manager Data - -Loading managers and linking employees to these managers is straightforward: - -==== -[source,java] ----- -include::src/main/java/com/greglturnquist/payroll/DatabaseLoader.java[tag=code] ----- -==== - -The one wrinkle is that Spring Security is active with access rules in full force when this loader runs. Thus, to save employee data, you must use Spring Security's `setAuthentication()` API to authenticate this loader with the proper name and role. At the end, the security context is cleared out. - -== Touring Your Secured REST Service - -With all these modifications in place, you can start the application (`./mvnw spring-boot:run`) and check out the modifications by using the following curl (shown with its output): - -==== ----- -$ curl -v -u greg:turnquist localhost:8080/api/employees/1 -* Trying ::1... -* Connected to localhost (::1) port 8080 (#0) -* Server auth using Basic with user 'greg' -> GET /api/employees/1 HTTP/1.1 -> Host: localhost:8080 -> Authorization: Basic Z3JlZzp0dXJucXVpc3Q= -> User-Agent: curl/7.43.0 -> Accept: */* -> -< HTTP/1.1 200 OK -< Server: Apache-Coyote/1.1 -< X-Content-Type-Options: nosniff -< X-XSS-Protection: 1; mode=block -< Cache-Control: no-cache, no-store, max-age=0, must-revalidate -< Pragma: no-cache -< Expires: 0 -< X-Frame-Options: DENY -< Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly -< ETag: "0" -< Content-Type: application/hal+json;charset=UTF-8 -< Transfer-Encoding: chunked -< Date: Tue, 25 Aug 2015 15:57:34 GMT -< -{ - "firstName" : "Frodo", - "lastName" : "Baggins", - "description" : "ring bearer", - "manager" : { - "name" : "greg", - "roles" : [ "ROLE_MANAGER" ] - }, - "_links" : { - "self" : { - "href" : "http://localhost:8080/api/employees/1" - } - } -} ----- -==== - -This shows a lot more detail than you saw in the first section. First of all, Spring Security turns on several HTTP protocols to protect against various attack vectors (Pragma, Expires, X-Frame-Options, and others). You are also issuing BASIC credentials with `-u greg:turnquist` which renders the Authorization header. - -Amidst all the headers, you can see the `ETag` header from your versioned resource. - -Finally, inside the data itself, you can see a new attribute: `manager`. You can see that it includes the name and roles but NOT the password. That is due to using `@JsonIgnore` on that field. Because Spring Data REST did not export that repository, its values are inlined in this resource. You will put that to good use as you update the UI in the next section. - -== Displaying Manager Information in the UI - -With all these modifications in the backend, you can now shift to updating things in the frontend. First of all, you can show an employee's manager inside the `` React component: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=employee] ----- -==== - -This merely adds a column for `this.props.employee.entity.manager.name`. - -== Filtering out JSON Schema Metadata - -If a field is shown in the data output, it is safe to assume it has an entry in the JSON Schema metadata. You can see it in the following excerpt: - -==== ----- -{ - ... - "manager" : { - "readOnly" : false, - "$ref" : "#/descriptors/manager" - }, - ... - }, - ... - "$schema" : "https://json-schema.org/draft-04/schema#" -} ----- -==== - -The `manager` field is not something you want people to edit directly. Since it is inlined, it should be viewed as a read-only attribute. To filter out inlined entries from the `CreateDialog` and `UpdateDialog`, you can delete such entries after fetching the JSON Schema metadata in `loadFromServer()`: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=json-schema-filter] ----- -==== - -This code trims out both URI relations as well as $ref entries. - -== Trapping for Unauthorized Access - -With security checks configured on the backend, you can add a handler in case someone tries to update a record without authorization: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=on-update] ----- -==== - -You had code to catch an HTTP 412 error. This traps an HTTP 403 status code and provides a suitable alert. - -You can do the same for delete operations: - -==== -[source,javascript,indent=0] ----- -include::src/main/js/app.js[tag=on-delete] ----- -==== - -This is coded similarly with a tailored error message. - -== Add some security details to the UI - -The last thing to crown this version of the application is to display who is logged in as well provide a logout button by including this new `
` in the `index.html` file ahead of the `react` `
`: - -==== -[source,html,indent=0] ----- -include::src/main/resources/templates/index.html[tag=logout] ----- -==== - -== Putting It All Together - -To see these changes in the frontend, restart the application and navigate to http://localhost:8080. - -You are immediately redirected to a login form. This form is supplied by Spring Security, though you can https://spring.io/guides/gs/securing-web/[create your own] if you wish. Log in as `greg` / `turnquist`, as the following image shows: - -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/security/images/security-1.png[] - -You can see the newly added manager column. Go through a couple pages until you find employees owned by *oliver*, as the following image shows: - -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/security/images/security-2.png[] - -Click on *Update*, make some changes, and then click *Update* again. It should fail with the following pop-up: - -image::https://github.com/spring-guides/tut-react-and-spring-data-rest/raw/master/security/images/security-3.png[] - -If you try *Delete*, it should fail with a similar message. If you create a new employee, it should be assigned to you. - -== Review - -In this section, you: - -* Defined the model of `manager` and linked it to an employee through a 1-to-many relationship. -* Created a repository for managers and told Spring Data REST to not export. -* Wrote a set of access rules for the employee repository and also write a security policy. -* Wrote another Spring Data REST event handler to trap creation events before they happen so that the current user could be assigned as the employee's manager. -* Updated the UI to show an employee's manager and also display error pop-ups when unauthorized actions are taken. - -Issues? - -The webpage has become quite sophisticated. But what about managing relationships and inlined data? The create and update dialogs are not really suited for that. It might require some custom written forms. - -Managers have access to employee data. Should employees have access? If you were to add more details like phone numbers and addresses, how would you model it? How would you grant employees access to the system so they could update those specific fields? Are there more hypermedia controls that would be handy to put on the page? diff --git a/security/images/security-1.png b/security/images/security-1.png deleted file mode 100644 index f80d2e7..0000000 Binary files a/security/images/security-1.png and /dev/null differ diff --git a/security/images/security-2.png b/security/images/security-2.png deleted file mode 100644 index c95e100..0000000 Binary files a/security/images/security-2.png and /dev/null differ diff --git a/security/images/security-3.png b/security/images/security-3.png deleted file mode 100644 index d36d59b..0000000 Binary files a/security/images/security-3.png and /dev/null differ diff --git a/security/mvnw b/security/mvnw deleted file mode 100755 index a16b543..0000000 --- a/security/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/security/mvnw.cmd b/security/mvnw.cmd deleted file mode 100644 index c8d4337..0000000 --- a/security/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/security/package.json b/security/package.json deleted file mode 100644 index 459c6c1..0000000 --- a/security/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "spring-data-rest-and-reactjs", - "version": "0.1.0", - "description": "Demo of ReactJS + Spring Data REST", - "repository": { - "type": "git", - "url": "git@github.com:spring-guides/tut-react-and-spring-data-rest.git" - }, - "keywords": [ - "rest", - "hateoas", - "spring", - "data", - "react" - ], - "author": "Greg L. Turnquist", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues" - }, - "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest", - "dependencies": { - "react": "^16.5.2", - "react-dom": "^16.5.2", - "sockjs-client": "^1.0.3", - "rest": "^1.3.1", - "stompjs": "^2.3.3" - }, - "scripts": { - "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js" - }, - "devDependencies": { - "@babel/core": "^7.1.0", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "babel-loader": "^8.0.2", - "webpack": "^4.19.1", - "webpack-cli": "^3.1.0" - } -} diff --git a/security/pom.xml b/security/pom.xml deleted file mode 100644 index f323783..0000000 --- a/security/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - 4.0.0 - - - com.greglturnquist - react-and-spring-data-rest - 0.0.1-SNAPSHOT - - - react-and-spring-data-rest-security - 0.0.1-SNAPSHOT - - React.js and Spring Data REST - Security - An SPA with ReactJS in the frontend and Spring Data REST in the backend - - - UTF-8 - 1.8 - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-data-rest - - - org.springframework.boot - spring-boot-starter-websocket - - - org.springframework.boot - spring-boot-devtools - - - - org.springframework.boot - spring-boot-starter-security - - - org.thymeleaf.extras - thymeleaf-extras-springsecurity5 - - - - com.h2database - h2 - runtime - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - com.github.eirslett - frontend-maven-plugin - - - - - \ No newline at end of file diff --git a/security/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java b/security/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java deleted file mode 100644 index 960d757..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/DatabaseLoader.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -public class DatabaseLoader implements CommandLineRunner { - - private final EmployeeRepository employees; - private final ManagerRepository managers; - - @Autowired - public DatabaseLoader(EmployeeRepository employeeRepository, - ManagerRepository managerRepository) { - - this.employees = employeeRepository; - this.managers = managerRepository; - } - - @Override - public void run(String... strings) throws Exception { - - Manager greg = this.managers.save(new Manager("greg", "turnquist", - "ROLE_MANAGER")); - Manager oliver = this.managers.save(new Manager("oliver", "gierke", - "ROLE_MANAGER")); - - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("greg", "doesn't matter", - AuthorityUtils.createAuthorityList("ROLE_MANAGER"))); - - this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg)); - this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg)); - this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg)); - - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken("oliver", "doesn't matter", - AuthorityUtils.createAuthorityList("ROLE_MANAGER"))); - - this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver)); - this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver)); - this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver)); - - SecurityContextHolder.clearContext(); - } -} -// end::code[] \ No newline at end of file diff --git a/security/src/main/java/com/greglturnquist/payroll/Employee.java b/security/src/main/java/com/greglturnquist/payroll/Employee.java deleted file mode 100644 index 9f7950d..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/Employee.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.ManyToOne; -import javax.persistence.Version; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Entity -public class Employee { - - private @Id @GeneratedValue Long id; - private String firstName; - private String lastName; - private String description; - - private @Version @JsonIgnore Long version; - - private @ManyToOne Manager manager; // <1> - - private Employee() {} - - public Employee(String firstName, String lastName, String description, Manager manager) { // <2> - this.firstName = firstName; - this.lastName = lastName; - this.description = description; - this.manager = manager; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Employee employee = (Employee) o; - return Objects.equals(id, employee.id) && - Objects.equals(firstName, employee.firstName) && - Objects.equals(lastName, employee.lastName) && - Objects.equals(description, employee.description) && - Objects.equals(version, employee.version) && - Objects.equals(manager, employee.manager); - } - - @Override - public int hashCode() { - - return Objects.hash(id, firstName, lastName, description, version, manager); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - public Manager getManager() { - return manager; - } - - public void setManager(Manager manager) { - this.manager = manager; - } - - @Override - public String toString() { - return "Employee{" + - "id=" + id + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", description='" + description + '\'' + - ", version=" + version + - ", manager=" + manager + - '}'; - } -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java b/security/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java deleted file mode 100644 index e2e4f48..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/EmployeeRepository.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.repository.query.Param; -import org.springframework.security.access.prepost.PreAuthorize; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@PreAuthorize("hasRole('ROLE_MANAGER')") // <1> -public interface EmployeeRepository extends PagingAndSortingRepository { - - @Override - @PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name") - Employee save(@Param("employee") Employee employee); - - @Override - @PreAuthorize("@employeeRepository.findById(#id)?.manager?.name == authentication?.name") - void deleteById(@Param("id") Long id); - - @Override - @PreAuthorize("#employee?.manager?.name == authentication?.name") - void delete(@Param("employee") Employee employee); - -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/EventHandler.java b/security/src/main/java/com/greglturnquist/payroll/EventHandler.java deleted file mode 100644 index d09b843..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/EventHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import static com.greglturnquist.payroll.WebSocketConfiguration.*; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.core.annotation.HandleAfterCreate; -import org.springframework.data.rest.core.annotation.HandleAfterDelete; -import org.springframework.data.rest.core.annotation.HandleAfterSave; -import org.springframework.data.rest.core.annotation.RepositoryEventHandler; -import org.springframework.hateoas.server.EntityLinks; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -@RepositoryEventHandler(Employee.class) -public class EventHandler { - - private final SimpMessagingTemplate websocket; - - private final EntityLinks entityLinks; - - @Autowired - public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) { - this.websocket = websocket; - this.entityLinks = entityLinks; - } - - @HandleAfterCreate - public void newEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/newEmployee", getPath(employee)); - } - - @HandleAfterDelete - public void deleteEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/deleteEmployee", getPath(employee)); - } - - @HandleAfterSave - public void updateEmployee(Employee employee) { - this.websocket.convertAndSend( - MESSAGE_PREFIX + "/updateEmployee", getPath(employee)); - } - - /** - * Take an {@link Employee} and get the URI using Spring Data REST's {@link EntityLinks}. - * - * @param employee - */ - private String getPath(Employee employee) { - return this.entityLinks.linkForItemResource(employee.getClass(), - employee.getId()).toUri().getPath(); - } - -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/HomeController.java b/security/src/main/java/com/greglturnquist/payroll/HomeController.java deleted file mode 100644 index abaaba2..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/HomeController.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Controller -public class HomeController { - - @RequestMapping(value = "/") - public String index() { - return "index"; - } - -} -// end::code[] \ No newline at end of file diff --git a/security/src/main/java/com/greglturnquist/payroll/Manager.java b/security/src/main/java/com/greglturnquist/payroll/Manager.java deleted file mode 100644 index 2445690..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/Manager.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import java.util.Arrays; -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Entity -public class Manager { - - public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); // <1> - - private @Id @GeneratedValue Long id; // <2> - - private String name; // <2> - - private @JsonIgnore String password; // <2> - - private String[] roles; // <2> - - public void setPassword(String password) { // <3> - this.password = PASSWORD_ENCODER.encode(password); - } - - protected Manager() {} - - public Manager(String name, String password, String... roles) { - - this.name = name; - this.setPassword(password); - this.roles = roles; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Manager manager = (Manager) o; - return Objects.equals(id, manager.id) && - Objects.equals(name, manager.name) && - Objects.equals(password, manager.password) && - Arrays.equals(roles, manager.roles); - } - - @Override - public int hashCode() { - - int result = Objects.hash(id, name, password); - result = 31 * result + Arrays.hashCode(roles); - return result; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPassword() { - return password; - } - - public String[] getRoles() { - return roles; - } - - public void setRoles(String[] roles) { - this.roles = roles; - } - - @Override - public String toString() { - return "Manager{" + - "id=" + id + - ", name='" + name + '\'' + - ", roles=" + Arrays.toString(roles) + - '}'; - } -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/ManagerRepository.java b/security/src/main/java/com/greglturnquist/payroll/ManagerRepository.java deleted file mode 100644 index 4302481..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/ManagerRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.data.repository.Repository; -import org.springframework.data.rest.core.annotation.RepositoryRestResource; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@RepositoryRestResource(exported = false) -public interface ManagerRepository extends Repository { - - Manager save(Manager manager); - - Manager findByName(String name); - -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java b/security/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java deleted file mode 100644 index 7b2fc31..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@SpringBootApplication -public class ReactAndSpringDataRestApplication { - - public static void main(String[] args) { - SpringApplication.run(ReactAndSpringDataRestApplication.class, args); - } -} -// end::code[] \ No newline at end of file diff --git a/security/src/main/java/com/greglturnquist/payroll/SecurityConfiguration.java b/security/src/main/java/com/greglturnquist/payroll/SecurityConfiguration.java deleted file mode 100644 index ed9acca..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/SecurityConfiguration.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Configuration -@EnableWebSecurity // <1> -@EnableGlobalMethodSecurity(prePostEnabled = true) // <2> -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // <3> - - @Autowired - private SpringDataJpaUserDetailsService userDetailsService; // <4> - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth - .userDetailsService(this.userDetailsService) - .passwordEncoder(Manager.PASSWORD_ENCODER); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { // <5> - http - .authorizeRequests() - .antMatchers("/built/**", "/main.css").permitAll() - .anyRequest().authenticated() - .and() - .formLogin() - .defaultSuccessUrl("/", true) - .permitAll() - .and() - .httpBasic() - .and() - .csrf().disable() - .logout() - .logoutSuccessUrl("/"); - } - -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/SpringDataJpaUserDetailsService.java b/security/src/main/java/com/greglturnquist/payroll/SpringDataJpaUserDetailsService.java deleted file mode 100644 index a4d1468..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/SpringDataJpaUserDetailsService.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -public class SpringDataJpaUserDetailsService implements UserDetailsService { - - private final ManagerRepository repository; - - @Autowired - public SpringDataJpaUserDetailsService(ManagerRepository repository) { - this.repository = repository; - } - - @Override - public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { - Manager manager = this.repository.findByName(name); - return new User(manager.getName(), manager.getPassword(), - AuthorityUtils.createAuthorityList(manager.getRoles())); - } - -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/SpringDataRestEventHandler.java b/security/src/main/java/com/greglturnquist/payroll/SpringDataRestEventHandler.java deleted file mode 100644 index 20c5b25..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/SpringDataRestEventHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.rest.core.annotation.HandleBeforeCreate; -import org.springframework.data.rest.core.annotation.HandleBeforeSave; -import org.springframework.data.rest.core.annotation.RepositoryEventHandler; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -@RepositoryEventHandler(Employee.class) // <1> -public class SpringDataRestEventHandler { - - private final ManagerRepository managerRepository; - - @Autowired - public SpringDataRestEventHandler(ManagerRepository managerRepository) { - this.managerRepository = managerRepository; - } - - @HandleBeforeCreate - @HandleBeforeSave - public void applyUserInformationUsingSecurityContext(Employee employee) { - - String name = SecurityContextHolder.getContext().getAuthentication().getName(); - Manager manager = this.managerRepository.findByName(name); - if (manager == null) { - Manager newManager = new Manager(); - newManager.setName(name); - newManager.setRoles(new String[]{"ROLE_MANAGER"}); - manager = this.managerRepository.save(newManager); - } - employee.setManager(manager); - } -} -// end::code[] diff --git a/security/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java b/security/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java deleted file mode 100644 index 4505112..0000000 --- a/security/src/main/java/com/greglturnquist/payroll/WebSocketConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.greglturnquist.payroll; - -import org.springframework.messaging.simp.config.MessageBrokerRegistry; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; -import org.springframework.web.socket.config.annotation.StompEndpointRegistry; -import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; - -/** - * @author Greg Turnquist - */ -// tag::code[] -@Component -@EnableWebSocketMessageBroker -public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { - - static final String MESSAGE_PREFIX = "/topic"; - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/payroll").withSockJS(); - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker(MESSAGE_PREFIX); - registry.setApplicationDestinationPrefixes("/app"); - } -} -// end::code[] \ No newline at end of file diff --git a/security/src/main/js/api/uriListConverter.js b/security/src/main/js/api/uriListConverter.js deleted file mode 100644 index 8d9dc2e..0000000 --- a/security/src/main/js/api/uriListConverter.js +++ /dev/null @@ -1,19 +0,0 @@ -define(function() { - 'use strict'; - - /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ - return { - read: function(str /*, opts */) { - return str.split('\n'); - }, - write: function(obj /*, opts */) { - // If this is an Array, extract the self URI and then join using a newline - if (obj instanceof Array) { - return obj.map(resource => resource._links.self.href).join('\n'); - } else { // otherwise, just return the self URI - return obj._links.self.href; - } - } - }; - -}); diff --git a/security/src/main/js/api/uriTemplateInterceptor.js b/security/src/main/js/api/uriTemplateInterceptor.js deleted file mode 100644 index c16ba33..0000000 --- a/security/src/main/js/api/uriTemplateInterceptor.js +++ /dev/null @@ -1,18 +0,0 @@ -define(function(require) { - 'use strict'; - - const interceptor = require('rest/interceptor'); - - return interceptor({ - request: function (request /*, config, meta */) { - /* If the URI is a URI Template per RFC 6570 (https://tools.ietf.org/html/rfc6570), trim out the template part */ - if (request.path.indexOf('{') === -1) { - return request; - } else { - request.path = request.path.split('{')[0]; - return request; - } - } - }); - -}); \ No newline at end of file diff --git a/security/src/main/js/app.js b/security/src/main/js/app.js deleted file mode 100644 index 357abee..0000000 --- a/security/src/main/js/app.js +++ /dev/null @@ -1,481 +0,0 @@ -'use strict'; - -const React = require('react'); -const ReactDOM = require('react-dom'); -const when = require('when'); -const client = require('./client'); - -const follow = require('./follow'); // function to hop multiple links by "rel" - -const stompClient = require('./websocket-listener'); - -const root = '/api'; - -class App extends React.Component { - - constructor(props) { - super(props); - this.state = {employees: [], attributes: [], page: 1, pageSize: 2, links: {} - , loggedInManager: this.props.loggedInManager}; - this.updatePageSize = this.updatePageSize.bind(this); - this.onCreate = this.onCreate.bind(this); - this.onUpdate = this.onUpdate.bind(this); - this.onDelete = this.onDelete.bind(this); - this.onNavigate = this.onNavigate.bind(this); - this.refreshCurrentPage = this.refreshCurrentPage.bind(this); - this.refreshAndGoToLastPage = this.refreshAndGoToLastPage.bind(this); - } - - loadFromServer(pageSize) { - follow(client, root, [ - {rel: 'employees', params: {size: pageSize}}] - ).then(employeeCollection => { - return client({ - method: 'GET', - path: employeeCollection.entity._links.profile.href, - headers: {'Accept': 'application/schema+json'} - }).then(schema => { - // tag::json-schema-filter[] - /** - * Filter unneeded JSON Schema properties, like uri references and - * subtypes ($ref). - */ - Object.keys(schema.entity.properties).forEach(function (property) { - if (schema.entity.properties[property].hasOwnProperty('format') && - schema.entity.properties[property].format === 'uri') { - delete schema.entity.properties[property]; - } - else if (schema.entity.properties[property].hasOwnProperty('$ref')) { - delete schema.entity.properties[property]; - } - }); - - this.schema = schema.entity; - this.links = employeeCollection.entity._links; - return employeeCollection; - // end::json-schema-filter[] - }); - }).then(employeeCollection => { - this.page = employeeCollection.entity.page; - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { - return when.all(employeePromises); - }).done(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: pageSize, - links: this.links - }); - }); - } - - // tag::on-create[] - onCreate(newEmployee) { - follow(client, root, ['employees']).done(response => { - client({ - method: 'POST', - path: response.entity._links.self.href, - entity: newEmployee, - headers: {'Content-Type': 'application/json'} - }) - }) - } - // end::on-create[] - - // tag::on-update[] - onUpdate(employee, updatedEmployee) { - if(employee.entity.manager.name === this.state.loggedInManager) { - updatedEmployee["manager"] = employee.entity.manager; - client({ - method: 'PUT', - path: employee.entity._links.self.href, - entity: updatedEmployee, - headers: { - 'Content-Type': 'application/json', - 'If-Match': employee.headers.Etag - } - }).done(response => { - /* Let the websocket handler update the state */ - }, response => { - if (response.status.code === 403) { - alert('ACCESS DENIED: You are not authorized to update ' + - employee.entity._links.self.href); - } - if (response.status.code === 412) { - alert('DENIED: Unable to update ' + employee.entity._links.self.href + - '. Your copy is stale.'); - } - }); - } else { - alert("You are not authorized to update"); - } - } - // end::on-update[] - - // tag::on-delete[] - onDelete(employee) { - client({method: 'DELETE', path: employee.entity._links.self.href} - ).done(response => {/* let the websocket handle updating the UI */}, - response => { - if (response.status.code === 403) { - alert('ACCESS DENIED: You are not authorized to delete ' + - employee.entity._links.self.href); - } - }); - } - // end::on-delete[] - - onNavigate(navUri) { - client({ - method: 'GET', - path: navUri - }).then(employeeCollection => { - this.links = employeeCollection.entity._links; - this.page = employeeCollection.entity.page; - - return employeeCollection.entity._embedded.employees.map(employee => - client({ - method: 'GET', - path: employee._links.self.href - }) - ); - }).then(employeePromises => { - return when.all(employeePromises); - }).done(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: this.state.pageSize, - links: this.links - }); - }); - } - - updatePageSize(pageSize) { - if (pageSize !== this.state.pageSize) { - this.loadFromServer(pageSize); - } - } - - // tag::websocket-handlers[] - refreshAndGoToLastPage(message) { - follow(client, root, [{ - rel: 'employees', - params: {size: this.state.pageSize} - }]).done(response => { - if (response.entity._links.last !== undefined) { - this.onNavigate(response.entity._links.last.href); - } else { - this.onNavigate(response.entity._links.self.href); - } - }) - } - - refreshCurrentPage(message) { - follow(client, root, [{ - rel: 'employees', - params: { - size: this.state.pageSize, - page: this.state.page.number - } - }]).then(employeeCollection => { - this.links = employeeCollection.entity._links; - this.page = employeeCollection.entity.page; - - return employeeCollection.entity._embedded.employees.map(employee => { - return client({ - method: 'GET', - path: employee._links.self.href - }) - }); - }).then(employeePromises => { - return when.all(employeePromises); - }).then(employees => { - this.setState({ - page: this.page, - employees: employees, - attributes: Object.keys(this.schema.properties), - pageSize: this.state.pageSize, - links: this.links - }); - }); - } - // end::websocket-handlers[] - - // tag::register-handlers[] - componentDidMount() { - this.loadFromServer(this.state.pageSize); - stompClient.register([ - {route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage}, - {route: '/topic/updateEmployee', callback: this.refreshCurrentPage}, - {route: '/topic/deleteEmployee', callback: this.refreshCurrentPage} - ]); - } - // end::register-handlers[] - - render() { - return ( -
- - -
- ) - } -} - -class CreateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const newEmployee = {}; - this.props.attributes.forEach(attribute => { - newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onCreate(newEmployee); - this.props.attributes.forEach(attribute => { - ReactDOM.findDOMNode(this.refs[attribute]).value = ''; // clear out the dialog's inputs - }); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - return ( -
- Create - -
-
- X - -

Create new employee

- -
- {inputs} - -
-
-
-
- ) - } -} - -class UpdateDialog extends React.Component { - - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - const updatedEmployee = {}; - this.props.attributes.forEach(attribute => { - updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim(); - }); - this.props.onUpdate(this.props.employee, updatedEmployee); - window.location = "#"; - } - - render() { - const inputs = this.props.attributes.map(attribute => -

- -

- ); - - const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href; - - const isManagerCorrect = this.props.employee.entity.manager.name == this.props.loggedInManager; - - if (isManagerCorrect === false) { - return ( - - ) - } else { - return ( -
- Update - -
-
- X - -

Update an employee

- -
- {inputs} - -
-
-
-
- ) - } - } - -} - -class EmployeeList extends React.Component { - - constructor(props) { - super(props); - this.handleNavFirst = this.handleNavFirst.bind(this); - this.handleNavPrev = this.handleNavPrev.bind(this); - this.handleNavNext = this.handleNavNext.bind(this); - this.handleNavLast = this.handleNavLast.bind(this); - this.handleInput = this.handleInput.bind(this); - } - - handleInput(e) { - e.preventDefault(); - const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value; - if (/^[0-9]+$/.test(pageSize)) { - this.props.updatePageSize(pageSize); - } else { - ReactDOM.findDOMNode(this.refs.pageSize).value = pageSize.substring(0, pageSize.length - 1); - } - } - - handleNavFirst(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.first.href); - } - - handleNavPrev(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.prev.href); - } - - handleNavNext(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.next.href); - } - - handleNavLast(e) { - e.preventDefault(); - this.props.onNavigate(this.props.links.last.href); - } - - render() { - const pageInfo = this.props.page.hasOwnProperty("number") ? -

Employees - Page {this.props.page.number + 1} of {this.props.page.totalPages}

: null; - - const employees = this.props.employees.map(employee => - - ); - - const navLinks = []; - if ("first" in this.props.links) { - navLinks.push(); - } - if ("prev" in this.props.links) { - navLinks.push(); - } - if ("next" in this.props.links) { - navLinks.push(); - } - if ("last" in this.props.links) { - navLinks.push(); - } - - return ( -
- {pageInfo} - - - - - - - - - - - - {employees} - -
First NameLast NameDescriptionManager
-
- {navLinks} -
-
- ) - } -} - -// tag::employee[] -class Employee extends React.Component { - - constructor(props) { - super(props); - this.handleDelete = this.handleDelete.bind(this); - } - - handleDelete() { - this.props.onDelete(this.props.employee); - } - - render() { - return ( - - {this.props.employee.entity.firstName} - {this.props.employee.entity.lastName} - {this.props.employee.entity.description} - {this.props.employee.entity.manager.name} - - - - - - - - ) - } -} -// end::employee[] - -ReactDOM.render( - , - document.getElementById('react') -) - diff --git a/security/src/main/js/client.js b/security/src/main/js/client.js deleted file mode 100644 index dfecbea..0000000 --- a/security/src/main/js/client.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const rest = require('rest'); -const defaultRequest = require('rest/interceptor/defaultRequest'); -const mime = require('rest/interceptor/mime'); -const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); -const errorCode = require('rest/interceptor/errorCode'); -const baseRegistry = require('rest/mime/registry'); - -const registry = baseRegistry.child(); - -registry.register('text/uri-list', require('./api/uriListConverter')); -registry.register('application/hal+json', require('rest/mime/type/application/hal')); - -module.exports = rest - .wrap(mime, { registry: registry }) - .wrap(uriTemplateInterceptor) - .wrap(errorCode) - .wrap(defaultRequest, { headers: { 'Accept': 'application/hal+json' }}); diff --git a/security/src/main/js/follow.js b/security/src/main/js/follow.js deleted file mode 100644 index 59efe70..0000000 --- a/security/src/main/js/follow.js +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = function follow(api, rootPath, relArray) { - const root = api({ - method: 'GET', - path: rootPath - }); - - return relArray.reduce(function(root, arrayItem) { - const rel = typeof arrayItem === 'string' ? arrayItem : arrayItem.rel; - return traverseNext(root, rel, arrayItem); - }, root); - - function traverseNext (root, rel, arrayItem) { - return root.then(function (response) { - if (hasEmbeddedRel(response.entity, rel)) { - return response.entity._embedded[rel]; - } - - if(!response.entity._links) { - return []; - } - - if (typeof arrayItem === 'string') { - return api({ - method: 'GET', - path: response.entity._links[rel].href - }); - } else { - return api({ - method: 'GET', - path: response.entity._links[rel].href, - params: arrayItem.params - }); - } - }); - } - - function hasEmbeddedRel (entity, rel) { - return entity._embedded && entity._embedded.hasOwnProperty(rel); - } -}; diff --git a/security/src/main/js/websocket-listener.js b/security/src/main/js/websocket-listener.js deleted file mode 100644 index fe7b6b3..0000000 --- a/security/src/main/js/websocket-listener.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const SockJS = require('sockjs-client'); // <1> -require('stompjs'); // <2> - -function register(registrations) { - const socket = SockJS('/payroll'); // <3> - const stompClient = Stomp.over(socket); - stompClient.connect({}, function(frame) { - registrations.forEach(function (registration) { // <4> - stompClient.subscribe(registration.route, registration.callback); - }); - }); -} - -module.exports = { - register: register -}; - diff --git a/security/src/main/resources/application.properties b/security/src/main/resources/application.properties deleted file mode 100644 index d329a65..0000000 --- a/security/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.data.rest.base-path=/api \ No newline at end of file diff --git a/security/src/main/resources/static/main.css b/security/src/main/resources/static/main.css deleted file mode 100644 index c95a16d..0000000 --- a/security/src/main/resources/static/main.css +++ /dev/null @@ -1,69 +0,0 @@ -input.field { - width: 90%; -} - -table { - border-collapse: collapse; -} - -td, th { - border: 1px solid #999; - padding: 0.5rem; - text-align: left; -} - -/*Classes for creating dialogs*/ - -.modalDialog { - position: fixed; - font-family: Arial, Helvetica, sans-serif; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0,0,0,0.8); - z-index: 99999; - opacity:0; - -webkit-transition: opacity 400ms ease-in; - -moz-transition: opacity 400ms ease-in; - transition: opacity 400ms ease-in; - pointer-events: none; -} - -.modalDialog:target { - opacity:1; - pointer-events: auto; -} - -.modalDialog > div { - width: 400px; - position: relative; - margin: 10% auto; - padding: 5px 20px 13px 20px; - border-radius: 10px; - background: #fff; - background: -moz-linear-gradient(#fff, #999); - background: -webkit-linear-gradient(#fff, #999); - background: -o-linear-gradient(#fff, #999); -} - -.close { - background: #606061; - color: #FFFFFF; - line-height: 25px; - position: absolute; - right: -12px; - text-align: center; - top: -10px; - width: 24px; - text-decoration: none; - font-weight: bold; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; - border-radius: 12px; - -moz-box-shadow: 1px 1px 3px #000; - -webkit-box-shadow: 1px 1px 3px #000; - box-shadow: 1px 1px 3px #000; -} - -.close:hover { background: #00d9ff; } diff --git a/security/src/main/resources/templates/index.html b/security/src/main/resources/templates/index.html deleted file mode 100644 index 3571b90..0000000 --- a/security/src/main/resources/templates/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - ReactJS + Spring Data REST - - - - -
- Hello, user. -
- -
-
- - -
- - - - - \ No newline at end of file diff --git a/security/webpack.config.js b/security/webpack.config.js deleted file mode 100644 index 8f4b525..0000000 --- a/security/webpack.config.js +++ /dev/null @@ -1,31 +0,0 @@ -var path = require('path'); - -module.exports = { - entry: './src/main/js/app.js', - devtool: 'sourcemaps', - cache: true, - mode: 'development', - resolve: { - alias: { - 'stompjs': __dirname + '/node_modules' + '/stompjs/lib/stomp.js', - } - }, - output: { - path: __dirname, - filename: './src/main/resources/static/built/bundle.js' - }, - module: { - rules: [ - { - test: path.join(__dirname, '.'), - exclude: /(node_modules)/, - use: [{ - loader: 'babel-loader', - options: { - presets: ["@babel/preset-env", "@babel/preset-react"] - } - }] - } - ] - } -}; \ No newline at end of file