Compare View
Commits (2)
-
feat: 完成一个
Showing
33 changed files
Show diff stats
pom.xml
... | ... | @@ -5,21 +5,34 @@ |
5 | 5 | <parent> |
6 | 6 | <groupId>org.springframework.boot</groupId> |
7 | 7 | <artifactId>spring-boot-starter-parent</artifactId> |
8 | - <version>2.2.11.RELEASE</version> | |
8 | + <version>2.4.3</version> | |
9 | 9 | <relativePath/> <!-- lookup parent from repository --> |
10 | 10 | </parent> |
11 | - <groupId>com.example</groupId> | |
12 | - <artifactId>demo</artifactId> | |
11 | + <groupId>com.peony</groupId> | |
12 | + <artifactId>virtual-box</artifactId> | |
13 | 13 | <version>0.0.1-SNAPSHOT</version> |
14 | 14 | <name>mina</name> |
15 | - <description>Demo project for Spring Boot</description> | |
15 | + | |
16 | 16 | <properties> |
17 | 17 | <java.version>1.8</java.version> |
18 | 18 | </properties> |
19 | + | |
19 | 20 | <dependencies> |
21 | + | |
20 | 22 | <dependency> |
21 | 23 | <groupId>org.springframework.boot</groupId> |
22 | - <artifactId>spring-boot-starter-web</artifactId> | |
24 | + <artifactId>spring-boot-starter</artifactId> | |
25 | + </dependency> | |
26 | + | |
27 | + <dependency> | |
28 | + <groupId>org.springframework.boot</groupId> | |
29 | + <artifactId>spring-boot-configuration-processor</artifactId> | |
30 | + </dependency> | |
31 | + | |
32 | + <dependency> | |
33 | + <groupId>org.apache.mina</groupId> | |
34 | + <artifactId>mina-core</artifactId> | |
35 | + <version>2.1.4</version> | |
23 | 36 | </dependency> |
24 | 37 | |
25 | 38 | <dependency> |
... | ... | @@ -28,11 +41,14 @@ |
28 | 41 | <scope>runtime</scope> |
29 | 42 | <optional>true</optional> |
30 | 43 | </dependency> |
44 | + | |
31 | 45 | <dependency> |
32 | 46 | <groupId>org.projectlombok</groupId> |
33 | 47 | <artifactId>lombok</artifactId> |
48 | + <scope>provided</scope> | |
34 | 49 | <optional>true</optional> |
35 | 50 | </dependency> |
51 | + | |
36 | 52 | <dependency> |
37 | 53 | <groupId>org.springframework.boot</groupId> |
38 | 54 | <artifactId>spring-boot-starter-test</artifactId> |
... | ... | @@ -44,42 +60,6 @@ |
44 | 60 | </exclusion> |
45 | 61 | </exclusions> |
46 | 62 | </dependency> |
47 | - <!--导入模板引擎才能实现页面跳转--> | |
48 | - <dependency> | |
49 | - <groupId>org.springframework.boot</groupId> | |
50 | - <artifactId>spring-boot-starter-thymeleaf</artifactId> | |
51 | - </dependency> | |
52 | - <!--springbootSecurity--> | |
53 | - <dependency> | |
54 | - <groupId>org.springframework.boot</groupId> | |
55 | - <artifactId>spring-boot-starter-security</artifactId> | |
56 | - </dependency> | |
57 | - | |
58 | - <!--rabbitmq--> | |
59 | - <dependency> | |
60 | - <groupId>org.springframework.boot</groupId> | |
61 | - <artifactId>spring-boot-starter-amqp</artifactId> | |
62 | - </dependency> | |
63 | - | |
64 | - <!--spring-statemachine--> | |
65 | - <dependency> | |
66 | - <groupId>org.springframework.statemachine</groupId> | |
67 | - <artifactId>spring-statemachine-core</artifactId> | |
68 | - <version>1.2.0.RELEASE</version> | |
69 | - </dependency> | |
70 | - | |
71 | - <!--mina--> | |
72 | - <dependency> | |
73 | - <groupId>org.apache.mina</groupId> | |
74 | - <artifactId>mina-core</artifactId> | |
75 | - <version>2.1.3</version> | |
76 | - </dependency> | |
77 | - <dependency> | |
78 | - <groupId>org.apache.mina</groupId> | |
79 | - <artifactId>mina-integration-spring</artifactId> | |
80 | - <version>1.1.7</version> | |
81 | - </dependency> | |
82 | - | |
83 | 63 | |
84 | 64 | </dependencies> |
85 | 65 | ... | ... |
src/main/java/com/example/mina/Application.java
... | ... | @@ -3,6 +3,10 @@ package com.example.mina; |
3 | 3 | import org.springframework.boot.SpringApplication; |
4 | 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; |
5 | 5 | |
6 | +/** | |
7 | + * @author 杜云山 | |
8 | + * @date 2021/03/05 | |
9 | + */ | |
6 | 10 | @SpringBootApplication |
7 | 11 | public class Application { |
8 | 12 | ... | ... |
src/main/java/com/example/mina/base/AbstractHardwareDataBuffer.java
0 → 100644
... | ... | @@ -0,0 +1,92 @@ |
1 | +package com.example.mina.base; | |
2 | + | |
3 | +import com.example.mina.entity.Entry; | |
4 | + | |
5 | +/** | |
6 | + * @author 杜云山 | |
7 | + * @date 2021/03/05 | |
8 | + */ | |
9 | +public abstract class AbstractHardwareDataBuffer { | |
10 | + | |
11 | + protected int maxRow; | |
12 | + | |
13 | + protected int maxCol; | |
14 | + | |
15 | + protected int maxOffset; | |
16 | + | |
17 | + protected int maxAttenuate; | |
18 | + | |
19 | + protected Entry[][] matrixData; | |
20 | + | |
21 | + protected Entry[] offsetData; | |
22 | + | |
23 | + public AbstractHardwareDataBuffer() { | |
24 | + } | |
25 | + | |
26 | + public AbstractHardwareDataBuffer(int row, int col, int offset, int maxAttenuate) { | |
27 | + this.maxRow = row; | |
28 | + this.maxCol = col; | |
29 | + this.maxOffset = offset; | |
30 | + this.maxAttenuate = maxAttenuate; | |
31 | + } | |
32 | + | |
33 | + /** | |
34 | + * Set cross point attenuation value. Actually that is not correct. | |
35 | + * In case of LTE device, this is a ON/OFF status (0,1) | |
36 | + * | |
37 | + * @param row the row index. *** starts from 1 *** | |
38 | + * @param col the col index. *** starts from 1 *** | |
39 | + * @param val the attenuation value | |
40 | + */ | |
41 | + public synchronized void setAttenuation(int row, int col, int val) { | |
42 | + | |
43 | + if (val != -1 && (row < 1 || row > maxRow || col < 1 || col > maxCol)) { | |
44 | + return; | |
45 | + } | |
46 | + if (val == -1) { | |
47 | + val = maxAttenuate; | |
48 | + } | |
49 | + matrixData[row - 1][col - 1] = new Entry(row - 1, col - 1, "rr", val, false); | |
50 | + ; | |
51 | + } | |
52 | + | |
53 | + public int getAttenuation(int row, int col) { | |
54 | + return matrixData[row - 1][col - 1].getValue(); | |
55 | + } | |
56 | + | |
57 | + public synchronized void setOffset(int row, int val) { | |
58 | + if (row < 1 || row > maxOffset) { | |
59 | + return; | |
60 | + } | |
61 | + offsetData[row - 1] = new Entry(row - 1, 0, "rr", val, false); | |
62 | + } | |
63 | + | |
64 | + public int getOffset(int row) { | |
65 | + return offsetData[row - 1].getValue(); | |
66 | + } | |
67 | + | |
68 | + public Entry[] getOffsetData() { | |
69 | + return offsetData; | |
70 | + } | |
71 | + | |
72 | + public Entry[][] getMatrixData() { | |
73 | + return matrixData; | |
74 | + } | |
75 | + | |
76 | + public int getMaxRow() { | |
77 | + return maxRow; | |
78 | + } | |
79 | + | |
80 | + public int getMaxCol() { | |
81 | + return maxCol; | |
82 | + } | |
83 | + | |
84 | + public int getMaxOffset() { | |
85 | + return maxOffset; | |
86 | + } | |
87 | + | |
88 | + public int getMaxAttenuate() { | |
89 | + return maxAttenuate; | |
90 | + } | |
91 | + | |
92 | +} | ... | ... |
src/main/java/com/example/mina/base/AbstractVirtualBoxHandler.java
0 → 100644
... | ... | @@ -0,0 +1,25 @@ |
1 | +package com.example.mina.base; | |
2 | + | |
3 | +import org.apache.mina.core.service.IoHandlerAdapter; | |
4 | + | |
5 | +/** | |
6 | + * @author 杜云山 | |
7 | + * @date 21/03/05 | |
8 | + */ | |
9 | +public abstract class AbstractVirtualBoxHandler extends IoHandlerAdapter { | |
10 | + | |
11 | + /** | |
12 | + * 初始化矩阵以及该设备的一些参数 | |
13 | + */ | |
14 | + protected abstract void initMatrix(); | |
15 | + | |
16 | + /** | |
17 | + * 处理消息 | |
18 | + * | |
19 | + * @param cmd 指令数据 | |
20 | + * @param len 数据长度 | |
21 | + * @return 返回消息 | |
22 | + */ | |
23 | + protected abstract byte[] handleMessage(byte[] cmd, int len); | |
24 | + | |
25 | +} | ... | ... |
src/main/java/com/example/mina/box1/AeroflexVirtualBoxHandler.java
0 → 100644
... | ... | @@ -0,0 +1,139 @@ |
1 | +package com.example.mina.box1; | |
2 | + | |
3 | +import com.example.mina.base.AbstractVirtualBoxHandler; | |
4 | +import com.example.mina.entity.AeroflexDataBuffer; | |
5 | +import com.example.mina.util.StrUtil; | |
6 | +import lombok.extern.slf4j.Slf4j; | |
7 | +import org.apache.mina.core.buffer.IoBuffer; | |
8 | +import org.apache.mina.core.session.IoSession; | |
9 | + | |
10 | +/** | |
11 | + * @author 杜云山 | |
12 | + * @date 21/03/05 | |
13 | + */ | |
14 | +@Slf4j | |
15 | +public class AeroflexVirtualBoxHandler extends AbstractVirtualBoxHandler { | |
16 | + | |
17 | + private static final byte[] ERROR = "ERROR".getBytes(); | |
18 | + | |
19 | + private static final byte[] NONE = "OK".getBytes(); | |
20 | + | |
21 | + private static final String SET_ALL = "ATTN ALL MAX"; | |
22 | + | |
23 | + private static final String SET_ONE = "ATTN"; | |
24 | + | |
25 | + private static final String GET_ONE = "ATTN?"; | |
26 | + | |
27 | + private static final String SPACE_SPLIT = " "; | |
28 | + | |
29 | + private static final String SEMICOLON_SPLIT = ";"; | |
30 | + | |
31 | + private AeroflexDataBuffer dataBuffer; | |
32 | + | |
33 | + public AeroflexVirtualBoxHandler() { | |
34 | + this.initMatrix(); | |
35 | + } | |
36 | + | |
37 | + @Override | |
38 | + protected void initMatrix() { | |
39 | + | |
40 | + int row = 10; | |
41 | + int col = 1; | |
42 | + int maxAttenuate = 888; | |
43 | + | |
44 | + dataBuffer = new AeroflexDataBuffer(row, maxAttenuate); | |
45 | + } | |
46 | + | |
47 | + @Override | |
48 | + public void messageReceived(IoSession session, Object message) { | |
49 | + | |
50 | + IoBuffer ioBuffer = (IoBuffer) message; | |
51 | + byte[] bytes = ioBuffer.array(); | |
52 | + | |
53 | + byte[] result = handleMessage(bytes, bytes.length); | |
54 | + | |
55 | + session.write(IoBuffer.wrap(result)); | |
56 | + } | |
57 | + | |
58 | + @Override | |
59 | + protected byte[] handleMessage(byte[] cmd, int len) { | |
60 | + String command = new String(cmd).trim(); | |
61 | + | |
62 | + log.info("aeroflexVirtualBoxHandler receive: {}", command); | |
63 | + | |
64 | + if (command.startsWith(SET_ALL)) { | |
65 | + //set all to max | |
66 | + for (int i = 1; i < dataBuffer.getMaxRow(); i++) { | |
67 | + dataBuffer.setOffset(i, dataBuffer.getMaxAttenuate()); | |
68 | + } | |
69 | + | |
70 | + return NONE; | |
71 | + } else if (command.startsWith(GET_ONE)) { | |
72 | + //get | |
73 | + String[] sss = command.split(SPACE_SPLIT); | |
74 | + if (sss.length >= 2) { | |
75 | + | |
76 | + int row = StrUtil.toInt(sss[1]); | |
77 | + if (row >= 0 && row <= dataBuffer.getMaxRow()) { | |
78 | + String str = String.valueOf(dataBuffer.getOffset(row)); | |
79 | + log.info("aeroflexVirtualBoxHandler return: {}", str); | |
80 | + return str.getBytes(); | |
81 | + } | |
82 | + } | |
83 | + return ERROR; | |
84 | + | |
85 | + } else if (command.startsWith(SET_ONE)) { | |
86 | + //Set, Follow by ATTN? | |
87 | + String[] aa = command.split(SEMICOLON_SPLIT); | |
88 | + | |
89 | + String[] sss = aa[0].split(SPACE_SPLIT); | |
90 | + | |
91 | + if (sss.length >= 3) { | |
92 | + | |
93 | + int row = StrUtil.toInt(sss[1]); | |
94 | + int val = StrUtil.toInt(sss[2]); | |
95 | + | |
96 | + if (row >= 0 && row <= dataBuffer.getMaxRow()) { | |
97 | + if (val >= 0 && val <= dataBuffer.getMaxAttenuate()) { | |
98 | + dataBuffer.setOffset(row, val); | |
99 | + | |
100 | + String str = String.valueOf(dataBuffer.getOffset(row)); | |
101 | + log.info("aeroflexVirtualBoxHandler return =====> {}", str); | |
102 | + return str.getBytes(); | |
103 | + } | |
104 | + } | |
105 | + } | |
106 | + | |
107 | + return ERROR; | |
108 | + | |
109 | + } else { | |
110 | + return ERROR; | |
111 | + } | |
112 | + | |
113 | + } | |
114 | + | |
115 | + @Override | |
116 | + public void sessionCreated(IoSession session) { | |
117 | + | |
118 | + log.info("--- abstractVirtual server session created"); | |
119 | + } | |
120 | + | |
121 | + @Override | |
122 | + public void sessionOpened(IoSession session) { | |
123 | + | |
124 | + log.info("--- abstractVirtual server session Opened"); | |
125 | + } | |
126 | + | |
127 | + @Override | |
128 | + public void sessionClosed(IoSession session) { | |
129 | + | |
130 | + log.info("--- abstractVirtual server session Closed"); | |
131 | + } | |
132 | + | |
133 | + @Override | |
134 | + public void messageSent(IoSession session, Object message) { | |
135 | + | |
136 | + log.info("--- abstractVirtual 发送数据成功!{}", message); | |
137 | + } | |
138 | + | |
139 | +} | ... | ... |
src/main/java/com/example/mina/config/AeroflexVirtualBoxConfiguration.java
0 → 100644
... | ... | @@ -0,0 +1,50 @@ |
1 | +package com.example.mina.config; | |
2 | + | |
3 | +import com.example.mina.box1.AeroflexVirtualBoxHandler; | |
4 | +import com.example.mina.property.AeroflexVirtualProperties; | |
5 | +import lombok.extern.slf4j.Slf4j; | |
6 | +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; | |
7 | +import org.springframework.context.annotation.Configuration; | |
8 | + | |
9 | +import javax.annotation.PostConstruct; | |
10 | +import java.net.InetSocketAddress; | |
11 | + | |
12 | +/** | |
13 | + * @author 杜云山 | |
14 | + * @date 21/03/05 | |
15 | + */ | |
16 | +@Slf4j | |
17 | +@Configuration(proxyBeanMethods = false) | |
18 | +public class AeroflexVirtualBoxConfiguration { | |
19 | + | |
20 | + private final AeroflexVirtualProperties aeroflexVirtualProperties; | |
21 | + | |
22 | + public AeroflexVirtualBoxConfiguration(AeroflexVirtualProperties aeroflexVirtualProperties) { | |
23 | + this.aeroflexVirtualProperties = aeroflexVirtualProperties; | |
24 | + } | |
25 | + | |
26 | + @PostConstruct | |
27 | + public void init() { | |
28 | + | |
29 | + if (!aeroflexVirtualProperties.getEnable()) { | |
30 | + log.info("AeroflexVirtual服务端 配置未开启"); | |
31 | + return; | |
32 | + } | |
33 | + | |
34 | + if (aeroflexVirtualProperties.getPort() == null) { | |
35 | + log.info("AeroflexVirtual服务端 端口未配置"); | |
36 | + return; | |
37 | + } | |
38 | + | |
39 | + try { | |
40 | + NioSocketAcceptor acceptor = new NioSocketAcceptor(); | |
41 | + acceptor.setHandler(new AeroflexVirtualBoxHandler()); | |
42 | + acceptor.setReuseAddress(true); | |
43 | + acceptor.bind(new InetSocketAddress(aeroflexVirtualProperties.getPort())); | |
44 | + log.info("AeroflexVirtual服务端已经启动,监听端口: {}", aeroflexVirtualProperties.getPort()); | |
45 | + } catch (Exception e) { | |
46 | + log.error("无法启动AeroflexVirtual服务端, {}", e.getMessage(), e); | |
47 | + } | |
48 | + } | |
49 | + | |
50 | +} | ... | ... |
src/main/java/com/example/mina/entity/AeroflexDataBuffer.java
0 → 100644
... | ... | @@ -0,0 +1,29 @@ |
1 | +package com.example.mina.entity; | |
2 | + | |
3 | +import com.example.mina.base.AbstractHardwareDataBuffer; | |
4 | + | |
5 | +/** | |
6 | + * @author 杜云山 | |
7 | + * @date 2021/03/05 | |
8 | + */ | |
9 | +public class AeroflexDataBuffer extends AbstractHardwareDataBuffer { | |
10 | + | |
11 | + public AeroflexDataBuffer(int row, int maxAtten) { | |
12 | + super(row, row, row, maxAtten); | |
13 | + | |
14 | + matrixData = new Entry[row][row]; | |
15 | + offsetData = new Entry[row]; | |
16 | + | |
17 | + for (int i = 0; i < row; i++) { | |
18 | + for (int k = 0; k < row; k++) { | |
19 | + matrixData[i][k] = new Entry(i, k, "kk", maxAtten, false); | |
20 | + } | |
21 | + } | |
22 | + | |
23 | + for (int i = 0; i < row; i++) { | |
24 | + offsetData[i] = new Entry(i, 0, "rr", maxAtten, false); | |
25 | + } | |
26 | + | |
27 | + } | |
28 | + | |
29 | +} | ... | ... |
... | ... | @@ -0,0 +1,60 @@ |
1 | +package com.example.mina.entity; | |
2 | + | |
3 | +/** | |
4 | + * @author 杜云山 | |
5 | + * @date 2021/03/05 | |
6 | + */ | |
7 | +public class Entry { | |
8 | + | |
9 | + private final int row; | |
10 | + | |
11 | + private final int col; | |
12 | + | |
13 | + private final String name; | |
14 | + | |
15 | + private int value; | |
16 | + | |
17 | + private final boolean booked; | |
18 | + | |
19 | + private final long timestamp; | |
20 | + | |
21 | + public static final Entry EMPTY = new Entry(0, 0, null, 0, false); | |
22 | + | |
23 | + public Entry(int row, int col, String name, int value, boolean booked) { | |
24 | + this.row = row; | |
25 | + this.col = col; | |
26 | + this.name = name; | |
27 | + this.value = value; | |
28 | + this.booked = booked; | |
29 | + this.timestamp = System.nanoTime(); | |
30 | + } | |
31 | + | |
32 | + public int getRow() { | |
33 | + return row; | |
34 | + } | |
35 | + | |
36 | + public int getCol() { | |
37 | + return col; | |
38 | + } | |
39 | + | |
40 | + public String getName() { | |
41 | + return name; | |
42 | + } | |
43 | + | |
44 | + public int getValue() { | |
45 | + return value; | |
46 | + } | |
47 | + | |
48 | + public boolean isBooked() { | |
49 | + return booked; | |
50 | + } | |
51 | + | |
52 | + public void setValue(int v) { | |
53 | + this.value = v; | |
54 | + } | |
55 | + | |
56 | + public long getTimestamp() { | |
57 | + return this.timestamp; | |
58 | + } | |
59 | + | |
60 | +} | ... | ... |
src/main/java/com/example/mina/property/AeroflexVirtualProperties.java
0 → 100644
... | ... | @@ -0,0 +1,26 @@ |
1 | +package com.example.mina.property; | |
2 | + | |
3 | +import lombok.Getter; | |
4 | +import lombok.Setter; | |
5 | +import lombok.ToString; | |
6 | +import org.springframework.boot.context.properties.ConfigurationProperties; | |
7 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
8 | +import org.springframework.context.annotation.Configuration; | |
9 | + | |
10 | +/** | |
11 | + * @author 杜云山 | |
12 | + * @date 2021/03/05 | |
13 | + */ | |
14 | +@Getter | |
15 | +@Setter | |
16 | +@ToString | |
17 | +@ConfigurationProperties(prefix = "aeroflex-virtual") | |
18 | +@Configuration | |
19 | +@EnableConfigurationProperties(AeroflexVirtualProperties.class) | |
20 | +public class AeroflexVirtualProperties { | |
21 | + | |
22 | + private Boolean enable = false; | |
23 | + | |
24 | + private Integer port = null; | |
25 | + | |
26 | +} | |
0 | 27 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,63 @@ |
1 | +package com.example.mina.util; | |
2 | + | |
3 | +import lombok.extern.slf4j.Slf4j; | |
4 | + | |
5 | +/** | |
6 | + * @author 杜云山 | |
7 | + * @date 2021/03/05 | |
8 | + */ | |
9 | +@Slf4j | |
10 | +public class StrUtil { | |
11 | + | |
12 | + public static int[] parseCommaInts(String str) { | |
13 | + if (str == null) { | |
14 | + return new int[0]; | |
15 | + } | |
16 | + | |
17 | + String[] ss = str.split(","); | |
18 | + int[] ids = new int[ss.length]; | |
19 | + | |
20 | + int count = 0; | |
21 | + for (int i = 0; i < ss.length; i++) { | |
22 | + int p = toInt(ss[i]); | |
23 | + ids[i] = p; | |
24 | + if (p >= 0) { | |
25 | + count++; | |
26 | + } | |
27 | + } | |
28 | + | |
29 | + if (count < ss.length) { | |
30 | + int[] ids2 = new int[count]; | |
31 | + count = 0; | |
32 | + for (int i = 0; i < ss.length; i++) { | |
33 | + if (ids[i] >= 0) { | |
34 | + ids2[count++] = ids[i]; | |
35 | + } | |
36 | + } | |
37 | + | |
38 | + ids = ids2; | |
39 | + } | |
40 | + | |
41 | + return ids; | |
42 | + } | |
43 | + | |
44 | + public static int toInt(String str) { | |
45 | + if (isEmpty(str)) { | |
46 | + return -1; | |
47 | + } | |
48 | + | |
49 | + try { | |
50 | + float f = Float.parseFloat(str.trim()); | |
51 | + return (int) f; | |
52 | + } catch (Exception e) { | |
53 | + log.error("{}, 无法转换为数字", str, e); | |
54 | + return -1; | |
55 | + } | |
56 | + | |
57 | + } | |
58 | + | |
59 | + public static boolean isEmpty(String str) { | |
60 | + return str == null || str.trim().isEmpty(); | |
61 | + } | |
62 | + | |
63 | +} | ... | ... |
src/main/resources/application.yml
1 | -server: | |
2 | - port: 8080 | |
3 | - servlet: | |
4 | - context-path: /test | |
5 | - | |
6 | -# rabbitmq配置 | |
7 | - #spring: | |
8 | - # rabbitmq: | |
9 | - # host: localhost | |
10 | - # port: 5672 | |
11 | - # username: guest | |
12 | - # password: guest | |
13 | - # virtual-host: / | |
14 | 1 | \ No newline at end of file |
2 | +aeroflex-virtual: | |
3 | + enable: true | |
4 | + port: 9100 | |
15 | 5 | \ No newline at end of file | ... | ... |
src/main/resources/templates/index.html
src/main/resources/templates/views/level1/1.html
src/main/resources/templates/views/level1/2.html
src/main/resources/templates/views/level1/3.html
src/main/resources/templates/views/level2/1.html
src/main/resources/templates/views/level2/2.html
src/main/resources/templates/views/level2/3.html
src/main/resources/templates/views/level3/1.html
src/main/resources/templates/views/level3/2.html
src/main/resources/templates/views/level3/3.html