Skip to content

Cepr0/single-file-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Single-file full-featured Spring REST CRUD service

Just-for-fun project

REST service placed in the single java file.

It supports:

  • Relational database
  • ORM model
  • Repositories
  • Transactional service
  • CRUD operations
  • Exception handling
  • Separate logging

The project can serve as an illustrative example for learning purposes and be useful as a basis for other projects.

@Slf4j
@SpringBootApplication
@RestController
@Validated
@ControllerAdvice
@EnableJpaRepositories(considerNestedRepositories = true)
public class Application extends ResponseEntityExceptionHandler {

	private static ModelRepo modelRepo;
	private ModelService modelService;

	public Application(ModelRepo modelRepo, ModelService modelService) {
		Application.modelRepo = modelRepo;
		this.modelService = modelService;
		log.debug("[i] App constructor initialized.");
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
		TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
		log.debug("[i] Context initialized.");
	}

	/**
	 * The Application Ready Event handler - populate demo data
	 */
	@EventListener
	void appReadyHandler(ApplicationReadyEvent e) {
		log.debug("[i] 'App ready' event received.");
		if (modelRepo != null) {
			modelRepo.save(asList(
					new Model("model1"),
					new Model("model2")
			));
		}
	}

	// The REST controller methods
	//
	@GetMapping("/models")
	ResponseEntity<?> getAll() {
		log.debug("[i] Received get all models request");
		return ResponseEntity.ok(modelRepo.findAll());
	}

	@GetMapping("/models/{id}")
	ResponseEntity<?> getOne(@PathVariable("id") int id) {
		log.debug("[i] Received get one model request for model with id '{}'", id);
		Model found = modelRepo.findOne(id);
		if (found == null) {
			throw new ModelNotFoundException();
		}
		return ResponseEntity.ok(found);
	}

	@PostMapping("/models")
	ResponseEntity<?> post(@Valid @RequestBody Model model) {
		log.debug("[i] Received create new model request for '{}'", model);
		Model created = modelService.create(model);
		URI uri = linkTo(methodOn(Application.class).post(model)).slash(created.getId()).toUri();
		return ResponseEntity.created(uri).body(created);
	}

	@PatchMapping("/models/{id}")
	ResponseEntity<?> update(@PathVariable("id") int id, @Valid @RequestBody Model model) {
		log.debug("[i] Received update request for model with id '{}'", id);
		Model updated = modelService.update(id, model);
		return ResponseEntity.ok(updated);
	}

	@DeleteMapping("/models/{id}")
	ResponseEntity<?> delete(@PathVariable("id") int id) {
		log.debug("[i] Received delete request for model with id '{}'", id);
		modelService.delete(id);
		return ResponseEntity.noContent().build();
	}

	/**
	 * Custom 'model not found' exception with standard error message.
	 */
	@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Model with requested id was not found!")
	static class ModelNotFoundException extends RuntimeException {
	}

	/**
	 * Custom 'MethodArgumentNotValidException' handler with custom {@link ErrorMessage}
	 */
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest req) {
		List<Error> errors = ex.getBindingResult().getAllErrors().stream().map(Error::of).collect(toList());
		String error = "Validation error!";
		ErrorMessage errorMessage = ErrorMessage.of(status.value(), error, null, ((ServletWebRequest) req).getRequest().getServletPath(), errors);
		log.warn(error);
		return new ResponseEntity<>(errorMessage, headers, status);
	}

	/**
	 * Custom 'HttpRequestMethodNotSupportedException' handler with custom {@link ErrorMessage}
	 */
	@Override
	protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest req) {
		ResponseEntity<Object> response = super.handleHttpRequestMethodNotSupported(ex, headers, status, req);
		HttpHeaders updatedHeaders = response.getHeaders();
		String method = ex.getMethod();
		String[] supportedMethods = ex.getSupportedMethods();
		String error = String.format("Requested method '%s' not supported! Allowed methods: '%s'", method, StringUtils.arrayToCommaDelimitedString(supportedMethods));
		String path = ((ServletWebRequest) req).getRequest().getServletPath();
		ErrorMessage errorMessage = ErrorMessage.of(status.value(), error, ex.getMessage(), path, null);
		return new ResponseEntity<>(errorMessage, updatedHeaders, status);
	}

	/**
	 * The Model service
	 */
	interface ModelService {
		Model create(Model model);
		void delete(int id);
		Model update(int id, Model model);
	}

	/**
	 * The Model service implementation
	 */
	@Service
	static class ModelServiceImpl implements ModelService {

		@Transactional
		public Model create(Model model) {
			Model created = modelRepo.saveAndFlush(model);
			log.debug("[i] Model created: {}", created);
			return created;
		}

		@Transactional
		public void delete(int id) {
			if (modelRepo.findOne(id) == null) {
				throw new ModelNotFoundException();
			}
			modelRepo.delete(id);
			modelRepo.flush();
			log.debug("[i] Model deleted.");
		}

		@Transactional
		public Model update(int id, Model model) {
			Model found = modelRepo.findOne(id);
			if (found == null) {
				throw new ModelNotFoundException();
			}
			found.setName(model.getName());
			modelRepo.saveAndFlush(found);
			log.debug("[i] Model updated: {}", found);
			return found;
		}
	}

	/**
	 * The repository
	 */
	@RepositoryRestResource(exported = false)
	interface ModelRepo extends JpaRepository<Model, Integer> {
	}

	/**
	 * The model
	 */
	@Data
	@NoArgsConstructor
	@Entity
	static class Model {
		@Id @GeneratedValue private Integer id;
		@NotBlank private String name;

		Model(String name) {
			this.name = name;
		}
	}

	/**
	 * Error message implementation.
	 * Example:
	 * <pre>
	 * {
	 *   "timestamp": "2018-02-04T18:37:04Z",
	 *   "status": 404,
	 *   "error": "Not Found",
	 *   "exception": "ModelNotFoundException",
	 *   "message": "Model with requested id was not found!",
	 *   "path": "/models/4"
	 * }
	 * </pre>
	 */
	@JsonInclude(NON_EMPTY)
	@JsonPropertyOrder({"timestamp", "status", "error", "message", "path", "errors"})
	@Value(staticConstructor = "of")
	private static class ErrorMessage {
		private Instant timestamp = Instant.now();
		private Integer status;
		private String error;
		private String message;
		private String path;
		private List<Error> errors;
	}

	/**
	 * Custom error. Example:
	 * <pre>
	 * {
	 *   "object": "model",
	 *   "property": "name",
	 *   "message": "must not be empty"
	 * }
	 * </pre>
	 */
	@JsonInclude(NON_EMPTY)
	@Value(staticConstructor = "of")
	private static class Error {
		private String object;
		private String property;
		private Object invalidValue;
		private String message;

		static Error of(ObjectError err) {
			if (err instanceof FieldError) {
				return new Error(err.getObjectName(), ((FieldError) err).getField(), ((FieldError) err).getRejectedValue(), err.getDefaultMessage());
			} else {
				return new Error(err.getObjectName(), null, null, err.getDefaultMessage());
			}
		}
	}
}

About

Single-file full-featured Spring REST CRUD service

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages