Привет Хабр! Это мой первый пост, который рассчитан на новичков в веб-программировании. Требуются знания:

— Java;
— Знать как создавать сервлет;
— HTML;
— JavaScript;
— JQuery.

Задача была такова:

— На основе данных БД построить дерево на веб-странице, все дерево сразу грузить было нельзя так, как слишком много данных.

В своем посте я покажу маленький поэтапный пример того, как построить дерево с помощью сервлета и плагина JsTree, с использованием Ajax в формате JSON, данные будут генерироваться по простому алгоритму.

Весь код доступен на GitHub, а так развернутый вариант на Heroku.



Модель


В таблице БД были записи типа {«id»,«parentId»,«text»}, для такого типа данных идеально подходит плагин JsTree. JsTree понимает данные в таком формате:

{
  id          : "string" // required
  parent      : "string" // required
  text        : "string" // node text
  icon        : "string" // string for custom
}

На деле оказалось, что нужно еще одно поле children. В него всегда надо ставить true, ничего страшного, если у элемента нет детей. После ajax запроса плагин это поймет и уберет стрелочку.

Для начала нам нужна модель:

public class Node {
  private String id;
  private String parent;
  private String text;
  private boolean children;   

    public Node(String id, String parent, String text) {
        this.id = id;
        this.parent = parent;
        this.text = text;
        this.children = true;
    }
}

Теперь стоит разобраться, как конвертировать данные в JSON, лично я использовал FasterXML/jackson-core. Для этого добавил зависимости в maven:

<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.2.2</version>
        </dependency>

И сам метод для конвертирования:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Map;

public class ConverterJSON {

    public static String toJSON_String(Map map) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(map.values());
    }

}

Чтобы работал метод, нужно расставить аннотации в Node:

import com.fasterxml.jackson.annotation.JsonProperty;

public class Node {
    @JsonProperty("id")
    private String id;
    @JsonProperty("parent")
    private String parent;
    @JsonProperty("text")
    private String text;
    @JsonProperty("children")
    private boolean children;

    public Node(String id, String parent, String text) {
        this.id = id;
        this.parent = parent;
        this.text = text;
        this.children = true;
    }

    public String getId() {
        return id;
    }
}

Замечательно, у нас есть модель, теперь нужны данные.

DAO


public interface Data {

    public Map getRoot();
    public Map getById(String parentId);

}

import java.util.HashMap;
import java.util.Map;
//Класс Синглтон 
public class LocalDataImpl implements Data  {
    public static final int MAX_ELEMENT = 10;

    private static LocalDataImpl ourInstance = new LocalDataImpl();

    public static LocalDataImpl getInstance() {
        return ourInstance;
    }

    private LocalDataImpl() {
    }

    @Override
    public Map getRoot() {         //Получем корень нашего дерева Это будет 10 элементов
        Map result = new HashMap();
        for (int i = 1; i < MAX_ELEMENT; i++) {
            Node node = new Node(Integer.toString(i),"#","Node "+i);      //В корне у JsTree элементы имеют родителя = "#"
            result.put(node.getId(),node);
        }
        return result;
    }

    @Override
    public Map getById(String parentId) {     //Получем детей нашего дерева Это будет тоже 10 элементов, при этом Если Родитель нечетный то у него не будей 
        Map result = new HashMap();
        if (Integer.parseInt(parentId)%2==0)
            for (int i = 1; i < MAX_ELEMENT; i++) {
                String newId = parentId+Integer.toString(i);
                Node node = new Node(newId, parentId,"Node "+newId);
                result.put(node.getId(),node);
            }
        return result;
    }
}

Controller


Теперь нужен сервлет, который будет принимать id узла родителя, и отдавать детей в формате JSON:

@WebServlet(urlPatterns = {"/ajax"})
public class AjaxTree extends HttpServlet {
    private Data data;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("application/json; charset=utf-8");
        String nodeId = request.getParameter("id");
        PrintWriter out = response.getWriter();
        try{
            if (nodeId.equals("root"))
                out.println(ConverterJSON.toJSON_String(data.getRoot()));
            else
                out.println(ConverterJSON.toJSON_String(data.getById(nodeId)));
        }catch (IOException e)
        {e.printStackTrace();}
        finally {
            out.close();
        }
    }

    @Override
    public void init() throws ServletException {
        super.init();
        data = LocalDataImpl.getInstance();
    }
}

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

Собрали проект и при использовании нашего сервлета видим:



Нечетный родитель — нет детей:



Четный — 9 штук:



Web


Теперь дело за малым, будем встраивать это чудо в страницу. Для этого нужно скачать Jquery и JsTree.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Только дерево</title>
    <link rel="stylesheet" href="themes/default/style.min.css" />
</head>
<body>
<!-- Подключаем Jquery -->
<script src="js/jquery-2.1.1.min.js"></script>
<!-- Подключаем JsTree-->
<script src="js/jstree.min.js"></script>
<!-- Плюс необходимый скрипт -->
<script>
    $(function () {
        $('#jstree')
                .jstree({
                    "plugins": [ "sort", "json_data" ],
                    'core': {
                        'data': {
                            'url': function (node) {
                                return node.id === '#' ? 'ajax?id=root' : 'ajax?id=' + node.id;  <!-- Первые адрес указывает откуда получить корень , Второй детей-->
                            },
                            'data': function (node) {
                                return { 'id': node.id };
                            }
                        }
                    }
                });
    });
</script>
<div id="jstree"></div>  <!-- Контейнер для расположения нашего дерева-->
</body>
</html>

Спасибо за внимание, надеюсь, кто-нибудь почерпнёт что-то полезное.
Поделиться с друзьями
-->

Комментарии (9)


  1. nerumb
    20.10.2016 19:57

    Задача такова:

    — На основе данных БД построить дерево на веб-странице, все дерево сразу грузить было нельзя так, как слишком много данных.

    Но при этом вы используете заглушку для работы с БД. Не придираюсь, но думаю что не стоит вводить путаницу.

    Чтобы работал метод, нужно расставить аннотации в Node:

    Не обязательно, будет работать и без них. Аннотации нужны чтобы указать названия полей под которыми данные будут представлены в json.

    Это мой первый пост, который рассчитан на новичков в веб-программировании

    Куда проще было бы воспользоваться spring boot, dropwizard, vert.x (и т.п.). И кода было бы гораздо меньше, да и понятней все для новичков, без всякого blood enterprises деплоя на сервер приложений, jsp и т.п.


    1. BioQwer
      21.10.2016 01:27

      Да вы правы. Основная задача отображение из БД, а для статьи на Хабр сделал простую заглушку.

      Не обязательно, будет работать и без них. Аннотации нужны чтобы указать названия полей под которыми данные будут представлены в json.

      Не всегда оно так, бывает задаешь правила для JacksonMapper и какие то куски API выходят из правил.
      Куда проще было бы воспользоваться spring boot, dropwizard, vert.x (и т.п.). И кода было бы гораздо меньше, да и понятней все для новичков, без всякого blood enterprises деплоя на сервер приложений, jsp и т.п.

      А jsp тут не используется и он мне сразу не понравился.
      Статью писал 2 года назад, тогда не знал про Spring, а когда узнал то ощутил blood enterprises деплой во всей красе.


  1. aol-nnov
    21.10.2016 01:11

    {
    id: «string» // required
    parent: «string» // required


    это всё «платина». все заборы исписаны такими примерами. вот, кабы nested sets там, или closure table…
    вот их рендеринг — это интересно!


    1. BioQwer
      21.10.2016 01:29
      +1

      Оно на heroku
      Позже начал играться с облаками вот оно там и осталось. Единственное сейчас там по 5к элементов генерируется.


  1. grossws
    21.10.2016 04:35
    +3

    public static String toJSON_String(Map map) throws JsonProcessingException

    Закачайте вашу java 1.4 обратно в Интернет, она там уже закончилась. Если что, 5 появилась в 2004, 12 (sic!) лет назад (и тогда же появились generics, да). Уже случился EOL Java 7 в прошлом году. Это даже не говоря про нормальное именование, в Java принят camelCase.


    //Класс Синглтон

    Загляните в effective java, прочитайте как обычно реализуют синглтоны, особенно в concurrent окружении, коим являются сервлеты. private static LocalDataImpl ourInstance — это некорректный вариант. Есть довольно удобный вариант с enum, но в 1.4 его ещё не завезли, если правильно помню. Но есть и вариант с private static final полем для экземпляра и private конструктором, хотя этот вариант не абсолютно надёжен.


    Если же смотреть глобально, то статья — такое днище, что маловероятно, что кто-нибудь сумеет постучать снизу.


  1. fogone
    21.10.2016 18:37
    +1

    Чуть более современный вариант серверной части
    class TreeNode {
        private String id;
        private String parentId;
        private String text;
        private boolean hasChildren;
    
        public TreeNode(String id, String parentId, String text, boolean hasChildren) {
            this.id = id;
            this.parentId = parentId;
            this.text = text;
            this.hasChildren = hasChildren;
        }
    
        public String getId() {
            return id;
        }
    
        public String getParentId() {
            return parentId;
        }
    
        public String getText() {
            return text;
        }
    
        public boolean hasChildren() {
            return hasChildren;
        }
    
        public boolean isChildOf(String parentId) {
            boolean isRoot = this.parentId == null && parentId == null;
            boolean isChild = this.parentId != null && this.parentId.equals(parentId);
    
            return isRoot || isChild;
        }
    }
    
    
    @ResponseBody
    @RequestMapping("/tree")
    class TreeController {
    
        private final TreeRepository treeRepository;
    
        @Autowired
        public TreeController(TreeRepository treeRepository) {
            this.treeRepository = treeRepository;
        }
    
        @RequestMapping("root")
        public List<TreeNode> root() {
            return treeRepository.findRoot();
        }
    
        @RequestMapping("node/{id}")
        public TreeNode get(@PathVariable("id") String id) {
            return treeRepository.findById(id);
        }
    
        @RequestMapping("node/{id}/children")
        public List<TreeNode> children(@PathVariable("id") String id) {
            return treeRepository.findChildrenByParentId(id);
        }
    
    }
    
    
    interface TreeRepository {
    
        TreeNode findById(String id);
    
        default List<TreeNode> findRoot() {
            return findChildrenByParentId(null);
        }
    
        List<TreeNode> findChildrenByParentId(String parentId);
    
    }
    
    class FakeTreeDataGenerator implements TreeDataGenerator {
    
        private final int count;
        private final int deep;
        private final IdGenerator idGenerator;
    
        public FakeTreeDataGenerator(IdGenerator idGenerator, int count, int deep) {
            this.idGenerator = idGenerator;
            this.count = count;
            this.deep = deep;
        }
    
        @Override
        public List<TreeNode> createTree() {
            return createFakeNodes(null, deep).collect(Collectors.toList());
        }
    
        private Stream<TreeNode> createFakeNodes(String parentId, int deep) {
            return IntStream.range(0, count)
                .boxed()
                .map(i -> idGenerator.generateId().toString())
                .flatMap(id -> {
                    boolean hasChildren = deep > 0;
                    Stream<TreeNode> nodeStream = Stream.of(
                        new TreeNode(id, parentId, id + " text", hasChildren));
                    return hasChildren ? 
                        Stream.concat(nodeStream, createFakeNodes(id, deep - 1)) : nodeStream;
                });
        }
    
    }
    
    class GeneratedTreeRepository implements TreeRepository {
    
        interface TreeDataGenerator {
    
            List<TreeNode> createTree();
    
        }
    
        private final Map<String, TreeNode> repository;
    
        @Autowired
        public GeneratedTreeRepository(TreeDataGenerator treeDataGenerator) {
            this.repository = treeDataGenerator
                .createTree()
                .stream()
                .collect(Collectors.toConcurrentMap(TreeNode::getId, Function.identity()));
        }
    
        @Override
        public TreeNode findById(String id) {
            return repository.get(id);
        }
    
        @Override
        public List<TreeNode> findChildrenByParentId(String parentId) {
            return repository.values().stream()
                .filter(treeNode -> treeNode.isChildOf(parentId))
                .collect(Collectors.toList());
        }
    
    }
    
    @SpringBootApplication
    public class TreeApplication {
    
        @Bean
        public TreeDataGenerator treeDataGenerator() {
            return new FakeTreeDataGenerator(new JdkIdGenerator(), 10, 5);
        }
    
        @Bean
        public TreeRepository treeRepository(TreeDataGenerator treeDataGenerator) {
            return new GeneratedTreeRepository(treeDataGenerator);
        }
    
        @Bean
        public TreeController treeController(TreeRepository treeRepository) {
            return new TreeController(treeRepository);
        }
    
        public static void main(String[] args) {
            SpringApplication.run(TreeApplication.class, args);
        }
    
    }
    


    1. fogone
      21.10.2016 18:56

      Прошу прощения, @Autowired на конструкторах явно лишние


    1. fogone
      21.10.2016 19:48
      +1

      Тоже самое но чуть более лаконично на котлине
      data class TreeNode(val id: String, val parentId: String?,
                          val text: String, val hasChildren: Boolean) {
      
          fun isChildOf(parentId: String?): Boolean {
              val isRoot = this.parentId == null && parentId == null
              val isChild = this.parentId != null && this.parentId == parentId
      
              return isRoot || isChild
          }
      
      }
      
      interface TreeRepository {
      
          fun findById(id: String): TreeNode?
      
          fun findRoot(): List<TreeNode> = findChildrenByParentId(null)
      
          fun findChildrenByParentId(parentId: String?): List<TreeNode>
      
      }
      
      interface TreeDataGenerator {
      
          fun createTree(): List<TreeNode>
      
      }
      
      class GeneratedTreeRepository(treeDataGenerator: TreeDataGenerator) : TreeRepository {
      
          private val repository = treeDataGenerator.createTree().associateBy(TreeNode::id)
      
          override fun findById(id: String): TreeNode? = repository[id]
      
          override fun findChildrenByParentId(parentId: String?): List<TreeNode> =
                  repository.values.filter { treeNode -> treeNode.isChildOf(parentId) }
      
      }
      
      class FakeTreeDataGenerator(val idGenerator: IdGenerator, val count: Int, val deep: Int) : TreeDataGenerator {
      
          override fun createTree(): List<TreeNode> = createFakeNodes(null, deep)
      
          private fun createFakeNodes(parentId: String?, deep: Int): List<TreeNode> =
                  if (deep >= 0) {
                      (0..count)
                              .map { i -> idGenerator.generateId().toString() }
                              .map { id -> TreeNode(id, parentId, id + " text", deep > 0) }
                              .flatMap { node -> createFakeNodes(node.id, deep - 1) + node }
                  } else emptyList()
      }
      
      @ResponseBody
      @RequestMapping("/tree")
      class TreeController(val treeRepository: TreeRepository) {
      
          @RequestMapping("root")
          fun root(): List<TreeNode> =
                  treeRepository.findRoot()
      
          @RequestMapping("node/{id}")
          fun get(@PathVariable("id") id: String): TreeNode? =
                  treeRepository.findById(id)
      
          @RequestMapping("node/{id}/children")
          fun children(@PathVariable("id") id: String): List<TreeNode> =
                  treeRepository.findChildrenByParentId(id)
      
      }
      
      @SpringBootApplication
      open class TreeApplication {
      
          @Bean
          open fun treeDataGenerator(): TreeDataGenerator =
                  FakeTreeDataGenerator(JdkIdGenerator(), 10, 5)
      
          @Bean
          open fun treeRepository(treeDataGenerator: TreeDataGenerator): TreeRepository =
                  GeneratedTreeRepository(treeDataGenerator)
      
          @Bean
          open fun treeController(treeRepository: TreeRepository): TreeController =
                  TreeController(treeRepository)
      
      }
      
      fun main(args: Array<String>) {
          SpringApplication.run(TreeApplication::class.java, *args)
      }
      


  1. AlexeyKh89
    22.10.2016 15:09

    ObjectMapper должен быть один, а не на каждый вызов метода.