— 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)
aol-nnov
21.10.2016 01:11{
id: «string» // required
parent: «string» // required
это всё «платина». все заборы исписаны такими примерами. вот, кабы nested sets там, или closure table…
вот их рендеринг — это интересно!BioQwer
21.10.2016 01:29+1Оно на heroku
Позже начал играться с облаками вот оно там и осталось. Единственное сейчас там по 5к элементов генерируется.
grossws
21.10.2016 04:35+3public 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
конструктором, хотя этот вариант не абсолютно надёжен.
Если же смотреть глобально, то статья — такое днище, что маловероятно, что кто-нибудь сумеет постучать снизу.
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); } }
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) }
nerumb
Но при этом вы используете заглушку для работы с БД. Не придираюсь, но думаю что не стоит вводить путаницу.
Не обязательно, будет работать и без них. Аннотации нужны чтобы указать названия полей под которыми данные будут представлены в json.
Куда проще было бы воспользоваться spring boot, dropwizard, vert.x (и т.п.). И кода было бы гораздо меньше, да и понятней все для новичков, без всякого
blood enterprisesдеплоя на сервер приложений, jsp и т.п.BioQwer
Да вы правы. Основная задача отображение из БД, а для статьи на Хабр сделал простую заглушку.
Не всегда оно так, бывает задаешь правила для JacksonMapper и какие то куски API выходят из правил.
А jsp тут не используется и он мне сразу не понравился.
Статью писал 2 года назад, тогда не знал про Spring, а когда узнал то ощутил blood enterprises деплой во всей красе.