Эксперимент: Todo API with SparkJava Framework

Эксперимент: Todo API with SparkJava Framework

Spark - микро фреймворк для создания веб приложений на Kotlin и Java 8 с минимальными усилием - как говорится на официальном сайте
!Не путать с Apache Spark-ом

Цель: показать старый пример CRUD приложения Rest Todo API через новый стек - в данном случае - Spark Java

Вот пример, простого веб приложения с endpoint-ом GET /hello

import static spark.Spark.*;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello Spark Framework");
    }
}

Запущенное приложение будет доступно по порту:
`http://localhost:4567/hello`


Итак, как и раньше нам нужно 4-5 endpoint-ов на создание, получение, удаление и редактирование /todos ресурсов

‌ - GET - /api/todo - получаем список объектов‌
‌ - POST - /api/todo - сохраняем объект‌
‌ - PUT - /api/todo - обновляем объект‌
‌ - DELETE - /api/todo/{id} - здесь мы удаляем объект по его id - который отмечен как переменная ресурса ‌

Создаем новый проект в Intelij Idea как Maven, в `pom.xml` добавлем следующую зависимость:

<dependencies>
        <!-- Главная зависимость для работы со Spark-ом       -->
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>2.8.0</version>
        </dependency>
        <!--        конвертируем объекты в json и обратно-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
    </dependencies>

Создаем класс TodoApplication - в нем у нас будет вся логика нашего  Rest API от endpoint-ов до модели и виртуального хранилища в виде ConcurrentMap-ы

package com.timurisachenko.todo;
import static spark.Spark.*;


public class TodoApplication {
    public static void main(String[] args) {
    	get("/todo", (req, res) -> "{\"id\": 1, \"text\": \"First task\", \"completed\":false}");
    }
}

отлично, можно запустить и проверить работу сервера по адресу ресурса /todo
Дальше лучше, видите какой вернулся json - наша модель будет такой же, в плане набора полей, создадим класс Todo:

private static class Todo {
        private Long id;
        private String text;
        private boolean completed;

        public Todo(Long id) {
            this.id = id;
        }

        public Todo(Long id, String text, boolean completed) {
            this.id = id;
            this.text = text;
            this.completed = completed;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public boolean isCompleted() {
            return completed;
        }

        public void setCompleted(boolean completed) {
            this.completed = completed;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Todo todo = (Todo) o;
            return completed == todo.completed &&
                    Objects.equals(id, todo.id) &&
                    Objects.equals(text, todo.text);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, text, completed);
        }
    }

Теперь мы можем делать Json преобразования как в нашу модель, так и обратно. Но чтобы сделать это более системно в фреймворке есть ResponseTransformator

get("/todo", (req, res) -> new Todo(1l, "First Task", false), 
		new ResponseTransformer() {
            private Gson gson = new Gson();
            @Override
            public String render(Object o) throws Exception {
                return gson.toJson(o);
            }
        });
post("/todo", (request, response) -> {    // Create something
});
put("/todo", (request, response) -> {    // Update something
});
delete("/todo/:id", (request, response) -> {    
// Annihilate something
});

Сделаем хранилище repository 😏 из ConcurrentMap-ы
и заполним тестовыми строками:

Gson gson = new Gson();
repo = Stream.of(
                new Todo(1l, "Привет это мой таск лист", false),
                new Todo(2l, "Мне нужно постоянно просматривать и сзаписывать задания ", false),
                new Todo(3l, "А это задание я уже выполнил", true))
                .collect(toConcurrentMap(f -> f.getId(), Function.identity()));
        idIncrement = new AtomicLong(repo.keySet().stream().max(Comparator.naturalOrder()).orElseGet(() -> 0l));

и обрамим все наши endpoint-ы в path("/api", () - { }); , таким образом получается что все наши endpoint-ы будут согласно первоначальному плану
👀👀👀 Итоговый класс:

package com.timuisachenko.todo;

import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.ResponseTransformer;

import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toConcurrentMap;
import static java.util.stream.Collectors.toList;
import static spark.Spark.*;

public class TodoApplication {
    private static ConcurrentMap<Long, Todo> repo;
    private static AtomicLong idIncrement;
    private static Logger LOGGER = LoggerFactory.getLogger(TodoApplication.class.getSimpleName());

    public static void main(String[] args) {
        Gson gson = new Gson();
        repo = Stream.of(
                new Todo(1l, "Привет это мой таск лист", false),
                new Todo(2l, "Мне нужно постоянно просматривать и сзаписывать задания ", false),
                new Todo(3l, "А это задание я уже выполнил", true))
                .collect(toConcurrentMap(f -> f.getId(), Function.identity()));
        idIncrement = new AtomicLong(repo.keySet().stream().max(Comparator.naturalOrder()).orElseGet(() -> 0l));

        path("/api", () -> {
            get("/todo", "application/json", (req, res) -> repo.keySet().stream().map(k -> repo.get(k)).collect(toList()), new ResponseTransformer() {
                @Override
                public String render(Object o) throws Exception {
                    return gson.toJson(o);
                }
            });
            post("/todo", (req, res) -> {
                Todo todo = gson.fromJson(req.body(), Todo.class);
                repo.putIfAbsent(idIncrement.incrementAndGet(), new Todo(idIncrement.longValue(), todo.getText(), false));
                return repo.get(idIncrement);
            });
            put("/todo",
                    (req, res) -> {
                        Todo todo = gson.fromJson(req.body(), Todo.class);
                        repo.put(todo.id, new Todo(todo.id, todo.getText(), todo.isCompleted()));
                        LOGGER.debug(todo.toString());
                        return repo.get(idIncrement);
                    }
            );
            delete("/todo/:id", (req, res) -> repo.remove(Long.parseLong(req.params(":id"))));

        });
    }

    private static class Todo {
        private Long id;
        private String text;
        private boolean completed;

        public Todo(Long id) {
            this.id = id;
        }

        public Todo(Long id, String text, boolean completed) {
            this.id = id;
            this.text = text;
            this.completed = completed;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public boolean isCompleted() {
            return completed;
        }

        public void setCompleted(boolean completed) {
            this.completed = completed;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Todo todo = (Todo) o;
            return completed == todo.completed &&
                    Objects.equals(id, todo.id) &&
                    Objects.equals(text, todo.text);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, text, completed);
        }
    }
}

повторю пример того, как можно протестировать из консоли браузера наш созданный Rest API:

//GET
await (await fetch("/api/todo", {
  method: "get",
  headers: {
    'Accept': 'application/json'
   }
})).json()
//POST
await (await fetch("/api/todo", {
  method: "post",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },

  //make sure to serialize your JSON body
  body: JSON.stringify({
    text: "Todo1",
    completed: false
  })
})).json()

//PUT
await (await fetch("/api/todo", {
  method: "put",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },

  //make sure to serialize your JSON body
  body: JSON.stringify({
    id: 1, 
    text: "Blablahblah",
    completed: true
  })
})).json()
//DELETE
await (await fetch("/api/todo/1", {
  method: "delete",
  headers: {
    'Accept': 'application/json'
   }
})).json()

Итак, что мы видим это, то что если вы работали(ете) с Node.Js и знакомы с языком Java + хотите воспользоваться множеством библиотечных решений платформы , то Spark Framework отличный выбор.
Будьте здоровы!
Премного благодарен за поддержку, шаринг и комменты 😁