第一次提交
This commit is contained in:
25
fastbee-server/coap-server/pom.xml
Normal file
25
fastbee-server/coap-server/pom.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>fastbee-server</artifactId>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<version>3.8.5</version>
|
||||
</parent>
|
||||
<artifactId>coap-server</artifactId>
|
||||
<name>coap-server</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<artifactId>iot-server-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>16.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,40 @@
|
||||
package com.fastbee.coap;
|
||||
|
||||
import com.fastbee.coap.handler.TimeResourceHandler;
|
||||
import com.fastbee.coap.server.CoapServerChannelInitializer;
|
||||
import com.fastbee.coap.server.ResourceRegistry;
|
||||
import com.fastbee.server.Server;
|
||||
import com.fastbee.server.config.NettyConfig;
|
||||
import io.netty.bootstrap.AbstractBootstrap;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class Coapserver extends Server {
|
||||
@Override
|
||||
protected AbstractBootstrap initialize() {
|
||||
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(config.name, Thread.MAX_PRIORITY));
|
||||
if (config.businessCore > 0) {
|
||||
businessService = new ThreadPoolExecutor(config.businessCore, config.businessCore, 1L,
|
||||
TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new DefaultThreadFactory(config.name, true, Thread.NORM_PRIORITY));
|
||||
}
|
||||
//注册数据路由
|
||||
ResourceRegistry resourceRegistry = new ResourceRegistry();
|
||||
resourceRegistry.register(new TimeResourceHandler("/utc-time"));
|
||||
return new Bootstrap()
|
||||
.group(bossGroup)
|
||||
.channel(NioDatagramChannel.class)
|
||||
.option(ChannelOption.SO_BROADCAST, true)
|
||||
.handler(new CoapServerChannelInitializer(resourceRegistry));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
## Coap
|
||||
|
||||
|
||||
## Coap Roadmap
|
||||
- Coap消息编解码 消息头 可选项 消息内容
|
||||
- Coap数据路由注册
|
||||
- Coap观察机制
|
||||
- Coap消息块处理
|
||||
- Coap认证处理 Token Messageid
|
||||
- Coap content上下文
|
||||
- Coap TransportService北向接口
|
||||
|
||||
## Coap南向接口
|
||||
- .well-known/core 查询服务端支持的uri资源
|
||||
- /utc-time 查询服务端utc-time
|
||||
- 其他拓展(设备注册,设备上下线,设备属性上报,设备)
|
||||
|
||||
## Coap北向接口 (TransportService 开放给业务层的接口类)
|
||||
- 设备命令下发
|
||||
- 设备属性设置
|
||||
- 设备升级
|
||||
- 设备重启
|
||||
|
||||
## Coap TransportContext 上下文
|
||||
- 转发器 (http转发,mqtt broker转发)
|
||||
- 消息格式适配器(物模型编码,json,字节编码,裸数据)
|
||||
- 调度器
|
||||
- 执行线程池
|
||||
- 请求消息
|
||||
- 响应消息
|
||||
- 会话管理
|
||||
|
||||
## Coap TransportHandler 处理函数
|
||||
- ReqDispatcher 消息分发处理
|
||||
|
@ -0,0 +1,222 @@
|
||||
package com.fastbee.coap.codec;
|
||||
|
||||
import com.fastbee.coap.model.*;
|
||||
import com.fastbee.coap.model.options.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.fastbee.coap.model.MessageCode.EMPTY;
|
||||
import static com.fastbee.coap.model.MessageType.*;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@Slf4j
|
||||
public class CoapMessageDecoder extends MessageToMessageDecoder<DatagramPacket> {
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket, List<Object> list) throws Exception {
|
||||
ByteBuf buffer = datagramPacket.content();
|
||||
log.debug("Incoming message to be decoded (length: {})", buffer.readableBytes());
|
||||
//Decode the header values
|
||||
int encodedHeader = buffer.readInt();
|
||||
int version = (encodedHeader >>> 30) & 0x03;
|
||||
int messageType = (encodedHeader >>> 28) & 0x03;
|
||||
int tokenLength = (encodedHeader >>> 24) & 0x0F;
|
||||
int messageCode = (encodedHeader >>> 16) & 0xFF;
|
||||
int messageID = (encodedHeader) & 0xFFFF;
|
||||
|
||||
|
||||
log.debug("Decoded Header: (T) {}, (TKL) {}, (C) {}, (ID) {}",
|
||||
messageType, tokenLength, messageCode, messageID);
|
||||
|
||||
//Check whether the protocol version is supported (=1)
|
||||
if (version != CoapMessage.PROTOCOL_VERSION) {
|
||||
String message = "CoAP version (" + version + ") is other than \"1\"!";
|
||||
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
|
||||
}
|
||||
|
||||
//Check whether TKL indicates a not allowed token length
|
||||
if (tokenLength > CoapMessage.MAX_TOKEN_LENGTH) {
|
||||
String message = "TKL value (" + tokenLength + ") is larger than 8!";
|
||||
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
|
||||
}
|
||||
|
||||
//Check whether there are enough unread bytes left to read the token
|
||||
if (buffer.readableBytes() < tokenLength) {
|
||||
String message = "TKL value is " + tokenLength + " but only " + buffer.readableBytes() + " bytes left!";
|
||||
throw new HeaderDecodingException(messageID, datagramPacket.sender(), message);
|
||||
}
|
||||
|
||||
//Handle empty message (ignore everything but the first 4 bytes)
|
||||
if (messageCode == EMPTY) {
|
||||
if (messageType == ACK) {
|
||||
log.info("ACK Message");
|
||||
list.add(CoapMessage.createEmptyAcknowledgement(messageID));
|
||||
return ;
|
||||
} else if (messageType == RST) {
|
||||
log.info("RST Message");
|
||||
list.add(CoapMessage.createEmptyReset(messageID));
|
||||
return;
|
||||
} else if (messageType == CON) {
|
||||
log.info("CON Message");
|
||||
list.add(CoapMessage.createPing(messageID));
|
||||
return;
|
||||
} else {
|
||||
//There is no empty NON message defined, so send a RST
|
||||
throw new HeaderDecodingException(messageID, datagramPacket.sender(), "Empty NON messages are invalid!");
|
||||
}
|
||||
}
|
||||
|
||||
//Read the token
|
||||
byte[] token = new byte[tokenLength];
|
||||
buffer.readBytes(token);
|
||||
|
||||
//Handle non-empty messages (CON, NON or ACK)
|
||||
CoapMessage coapMessage;
|
||||
|
||||
if (MessageCode.isRequest(messageCode)) {
|
||||
coapMessage = new CoapRequest(messageType, messageCode);
|
||||
} else {
|
||||
coapMessage = new CoapResponse(messageType, messageCode);
|
||||
coapMessage.setMessageType(messageType);
|
||||
}
|
||||
|
||||
coapMessage.setMessageID(messageID);
|
||||
coapMessage.setToken(new Token(token));
|
||||
|
||||
//Decode and set the options
|
||||
if (buffer.readableBytes() > 0) {
|
||||
try {
|
||||
setOptions(coapMessage, buffer);
|
||||
} catch (OptionCodecException ex) {
|
||||
ex.setMessageID(messageID);
|
||||
ex.setToken(new Token(token));
|
||||
//ex.setRemoteSocket(remoteSocket);
|
||||
ex.setMessageType(messageType);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
//The remaining bytes (if any) are the messages payload. If there is no payload, reader and writer index are
|
||||
//at the same position (buf.readableBytes() == 0).
|
||||
buffer.discardReadBytes();
|
||||
|
||||
try {
|
||||
coapMessage.setContent(buffer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
String warning = "Message code {} does not allow content. Ignore {} bytes.";
|
||||
log.warn(warning, coapMessage.getMessageCode(), buffer.readableBytes());
|
||||
}
|
||||
log.info("Decoded Message: {}", coapMessage);
|
||||
coapMessage.setSender(datagramPacket.sender());
|
||||
list.add(coapMessage);
|
||||
}
|
||||
|
||||
private void setOptions(CoapMessage coapMessage, ByteBuf buffer) throws OptionCodecException {
|
||||
|
||||
//Decode the options
|
||||
int previousOptionNumber = 0;
|
||||
int firstByte = buffer.readByte() & 0xFF;
|
||||
|
||||
while(firstByte != 0xFF && buffer.readableBytes() >= 0) {
|
||||
log.debug("First byte: {} ({})", toBinaryString(firstByte), firstByte);
|
||||
int optionDelta = (firstByte & 0xF0) >>> 4;
|
||||
int optionLength = firstByte & 0x0F;
|
||||
log.debug("temp. delta: {}, temp. length {}", optionDelta, optionLength);
|
||||
|
||||
if (optionDelta == 13) {
|
||||
optionDelta += buffer.readByte() & 0xFF;
|
||||
} else if (optionDelta == 14) {
|
||||
optionDelta = 269 + ((buffer.readByte() & 0xFF) << 8) + (buffer.readByte() & 0xFF);
|
||||
}
|
||||
|
||||
if (optionLength == 13) {
|
||||
optionLength += buffer.readByte() & 0xFF;
|
||||
} else if (optionLength == 14) {
|
||||
optionLength = 269 + ((buffer.readByte() & 0xFF) << 8) + (buffer.readByte() & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
log.info("Previous option: {}, Option delta: {}", previousOptionNumber, optionDelta);
|
||||
|
||||
int actualOptionNumber = previousOptionNumber + optionDelta;
|
||||
log.info("Decode option no. {} with length of {} bytes.", actualOptionNumber, optionLength);
|
||||
|
||||
try {
|
||||
byte[] optionValue = new byte[optionLength];
|
||||
buffer.readBytes(optionValue);
|
||||
|
||||
switch(OptionValue.getType(actualOptionNumber)) {
|
||||
case EMPTY: {
|
||||
EmptyOptionValue value = new EmptyOptionValue(actualOptionNumber);
|
||||
coapMessage.addOption(actualOptionNumber, value);
|
||||
break;
|
||||
}
|
||||
case OPAQUE: {
|
||||
OpaqueOptionValue value = new OpaqueOptionValue(actualOptionNumber, optionValue);
|
||||
coapMessage.addOption(actualOptionNumber, value);
|
||||
break;
|
||||
}
|
||||
case STRING: {
|
||||
StringOptionValue value = new StringOptionValue(actualOptionNumber, optionValue, true);
|
||||
coapMessage.addOption(actualOptionNumber, value);
|
||||
break;
|
||||
}
|
||||
case UINT: {
|
||||
UintOptionValue value = new UintOptionValue(actualOptionNumber, optionValue, true);
|
||||
coapMessage.addOption(actualOptionNumber, value);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
log.error("This should never happen!");
|
||||
throw new RuntimeException("This should never happen!");
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
//failed option creation leads to an illegal argument exception
|
||||
log.warn("Exception while decoding option!", e);
|
||||
|
||||
if (MessageCode.isResponse(coapMessage.getMessageCode())) {
|
||||
//Malformed options in responses are silently ignored...
|
||||
log.warn("Silently ignore malformed option no. {} in inbound response.", actualOptionNumber);
|
||||
} else if (Option.isCritical(actualOptionNumber)) {
|
||||
//Critical malformed options in requests cause an exception
|
||||
throw new OptionCodecException(actualOptionNumber);
|
||||
} else {
|
||||
//Not critical malformed options in requests are silently ignored...
|
||||
log.warn("Silently ignore elective option no. {} in inbound request.", actualOptionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
previousOptionNumber = actualOptionNumber;
|
||||
|
||||
if (buffer.readableBytes() > 0) {
|
||||
firstByte = buffer.readByte() & 0xFF;
|
||||
} else {
|
||||
// this is necessary if there is no payload and the last option is empty (e.g. UintOption with value 0)
|
||||
firstByte = 0xFF;
|
||||
}
|
||||
|
||||
log.debug("{} readable bytes remaining.", buffer.readableBytes());
|
||||
}
|
||||
}
|
||||
|
||||
private static String toBinaryString(int byteValue) {
|
||||
StringBuilder buffer = new StringBuilder(8);
|
||||
|
||||
for(int i = 7; i >= 0; i--) {
|
||||
if ((byteValue & (int) Math.pow(2, i)) > 0) {
|
||||
buffer.append("1");
|
||||
} else {
|
||||
buffer.append("0");
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package com.fastbee.coap.codec;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.MessageCode;
|
||||
import com.fastbee.coap.model.options.OptionValue;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@Slf4j
|
||||
public class CoapMessageEncoder extends MessageToMessageEncoder<CoapMessage> {
|
||||
/**
|
||||
* The maximum option delta (65804)
|
||||
*/
|
||||
public static final int MAX_OPTION_DELTA = 65804;
|
||||
|
||||
/**
|
||||
* The maximum option length (65804)
|
||||
*/
|
||||
public static final int MAX_OPTION_LENGTH = 65804;
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext channelHandlerContext, CoapMessage coapMessage, List<Object> list) throws Exception {
|
||||
log.info("CoapMessage to be encoded: {}", coapMessage);
|
||||
// start encoding
|
||||
ByteBuf encodedMessage = ByteBufAllocator.DEFAULT.buffer();
|
||||
// encode HEADER and TOKEN
|
||||
encodeHeader(encodedMessage, coapMessage);
|
||||
log.debug("Encoded length of message (after HEADER + TOKEN): {}", encodedMessage.readableBytes());
|
||||
|
||||
if (coapMessage.getMessageCode() == MessageCode.EMPTY) {
|
||||
encodedMessage = Unpooled.wrappedBuffer(Ints.toByteArray(encodedMessage.getInt(0) & 0xF0FFFFFF));
|
||||
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
|
||||
list.add(datagramPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coapMessage.getAllOptions().size() == 0 && coapMessage.getContent().readableBytes() == 0) {
|
||||
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
|
||||
list.add(datagramPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// encode OPTIONS (if any)
|
||||
encodeOptions(encodedMessage, coapMessage);
|
||||
log.debug("Encoded length of message (after OPTIONS): {}", encodedMessage.readableBytes());
|
||||
|
||||
// encode payload (if any)
|
||||
if (coapMessage.getContent().readableBytes() > 0) {
|
||||
// add END-OF-OPTIONS marker only if there is payload
|
||||
encodedMessage.writeByte(255);
|
||||
|
||||
// add payload
|
||||
encodedMessage = Unpooled.wrappedBuffer(encodedMessage, coapMessage.getContent());
|
||||
log.debug("Encoded length of message (after CONTENT): {}", encodedMessage.readableBytes());
|
||||
}
|
||||
|
||||
//发送响应
|
||||
DatagramPacket datagramPacket = new DatagramPacket(encodedMessage, coapMessage.getSender());
|
||||
list.add(datagramPacket);
|
||||
}
|
||||
protected void encodeHeader(ByteBuf buffer, CoapMessage coapMessage) {
|
||||
|
||||
byte[] token = coapMessage.getToken().getBytes();
|
||||
|
||||
int encodedHeader = ((coapMessage.getProtocolVersion() & 0x03) << 30)
|
||||
| ((coapMessage.getMessageType() & 0x03) << 28)
|
||||
| ((token.length & 0x0F) << 24)
|
||||
| ((coapMessage.getMessageCode() & 0xFF) << 16)
|
||||
| ((coapMessage.getMessageID() & 0xFFFF));
|
||||
|
||||
buffer.writeInt(encodedHeader);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
StringBuilder binary = new StringBuilder(Integer.toBinaryString(encodedHeader));
|
||||
while(binary.length() < 32) {
|
||||
binary.insert(0, "0");
|
||||
}
|
||||
log.debug("Encoded Header: {}", binary.toString());
|
||||
}
|
||||
|
||||
//Write token
|
||||
if (token.length > 0) {
|
||||
buffer.writeBytes(token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void encodeOptions(ByteBuf buffer, CoapMessage coapMessage) throws OptionCodecException {
|
||||
|
||||
//Encode options one after the other and append buf option to the buf
|
||||
int previousOptionNumber = 0;
|
||||
|
||||
for(int optionNumber : coapMessage.getAllOptions().keySet()) {
|
||||
for(OptionValue optionValue : coapMessage.getOptions(optionNumber)) {
|
||||
encodeOption(buffer, optionNumber, optionValue, previousOptionNumber);
|
||||
previousOptionNumber = optionNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void encodeOption(ByteBuf buffer, int optionNumber, OptionValue optionValue, int prevNumber)
|
||||
throws OptionCodecException {
|
||||
|
||||
//The previous option number must be smaller or equal to the actual one
|
||||
if (prevNumber > optionNumber) {
|
||||
log.error("Previous option no. ({}) must not be larger then current option no ({})",
|
||||
prevNumber, optionNumber);
|
||||
throw new OptionCodecException(optionNumber);
|
||||
}
|
||||
|
||||
int optionDelta = optionNumber - prevNumber;
|
||||
int optionLength = optionValue.getValue().length;
|
||||
|
||||
if (optionLength > MAX_OPTION_LENGTH) {
|
||||
log.error("Option no. {} exceeds maximum option length (actual: {}, max: {}).",
|
||||
optionNumber, optionLength, MAX_OPTION_LENGTH);
|
||||
|
||||
throw new OptionCodecException(optionNumber);
|
||||
}
|
||||
|
||||
if (optionDelta > MAX_OPTION_DELTA) {
|
||||
log.error("Option delta exceeds maximum option delta (actual: {}, max: {})", optionDelta, MAX_OPTION_DELTA);
|
||||
throw new OptionCodecException(optionNumber);
|
||||
}
|
||||
|
||||
if (optionDelta < 13) {
|
||||
//option delta < 13
|
||||
if (optionLength < 13) {
|
||||
buffer.writeByte(((optionDelta & 0xFF) << 4) | (optionLength & 0xFF));
|
||||
} else if (optionLength < 269) {
|
||||
buffer.writeByte(((optionDelta << 4) & 0xFF) | (13 & 0xFF));
|
||||
buffer.writeByte((optionLength - 13) & 0xFF);
|
||||
} else {
|
||||
buffer.writeByte(((optionDelta << 4) & 0xFF) | (14 & 0xFF));
|
||||
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionLength - 269) & 0xFF);
|
||||
}
|
||||
} else if (optionDelta < 269) {
|
||||
//13 <= option delta < 269
|
||||
if (optionLength < 13) {
|
||||
buffer.writeByte(((13 & 0xFF) << 4) | (optionLength & 0xFF));
|
||||
buffer.writeByte((optionDelta - 13) & 0xFF);
|
||||
} else if (optionLength < 269) {
|
||||
buffer.writeByte(((13 & 0xFF) << 4) | (13 & 0xFF));
|
||||
buffer.writeByte((optionDelta - 13) & 0xFF);
|
||||
buffer.writeByte((optionLength - 13) & 0xFF);
|
||||
} else {
|
||||
buffer.writeByte((13 & 0xFF) << 4 | (14 & 0xFF));
|
||||
buffer.writeByte((optionDelta - 13) & 0xFF);
|
||||
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionLength - 269) & 0xFF);
|
||||
}
|
||||
} else {
|
||||
//269 <= option delta < 65805
|
||||
if (optionLength < 13) {
|
||||
buffer.writeByte(((14 & 0xFF) << 4) | (optionLength & 0xFF));
|
||||
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionDelta - 269) & 0xFF);
|
||||
} else if (optionLength < 269) {
|
||||
buffer.writeByte(((14 & 0xFF) << 4) | (13 & 0xFF));
|
||||
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionDelta - 269) & 0xFF);
|
||||
buffer.writeByte((optionLength - 13) & 0xFF);
|
||||
} else {
|
||||
buffer.writeByte(((14 & 0xFF) << 4) | (14 & 0xFF));
|
||||
buffer.writeByte(((optionDelta - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionDelta - 269) & 0xFF);
|
||||
buffer.writeByte(((optionLength - 269) & 0xFF00) >>> 8);
|
||||
buffer.writeByte((optionLength - 269) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
//Write option value
|
||||
buffer.writeBytes(optionValue.getValue());
|
||||
log.debug("Encoded option no {} with value {}", optionNumber, optionValue.getDecodedValue());
|
||||
log.debug("Encoded message length is now: {}", buffer.readableBytes());
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.fastbee.coap.codec;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class HeaderDecodingException extends Exception{
|
||||
|
||||
private int messageID;
|
||||
private InetSocketAddress remoteSocket;
|
||||
|
||||
public HeaderDecodingException(int messageID, InetSocketAddress remoteSocket, String message) {
|
||||
super(message);
|
||||
this.messageID = messageID;
|
||||
this.remoteSocket = remoteSocket;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.fastbee.coap.codec;
|
||||
|
||||
import com.fastbee.coap.model.Token;
|
||||
import com.fastbee.coap.model.options.Option;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class OptionCodecException extends Exception {
|
||||
|
||||
private static final String message = "Unsupported or misplaced critical option %s";
|
||||
private int optionNumber;
|
||||
private int messageID;
|
||||
private Token token;
|
||||
private InetSocketAddress remoteSocket;
|
||||
private int messageType;
|
||||
|
||||
public OptionCodecException(int optionNumber) {
|
||||
super();
|
||||
this.optionNumber = optionNumber;
|
||||
}
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return String.format(message, Option.asString(this.optionNumber));
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.fastbee.coap.handler;
|
||||
|
||||
public abstract class AbstractResourceHandler implements ResourceHandler{
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInterface() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourceType() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.fastbee.coap.handler;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.CoapRequest;
|
||||
import com.fastbee.coap.server.ResourceRegistry;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@Slf4j
|
||||
@Setter
|
||||
@Getter
|
||||
public class ReqDispatcher extends SimpleChannelInboundHandler<CoapMessage> {
|
||||
|
||||
private ResourceRegistry resourceRegistry;
|
||||
private ChannelHandlerContext context;
|
||||
|
||||
public ReqDispatcher(ResourceRegistry resourceRegistry) {
|
||||
this.resourceRegistry = resourceRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext channelHandlerContext, CoapMessage coapMessage) throws Exception {
|
||||
log.debug("ReqDispatcher message: {}", coapMessage);
|
||||
if (!(coapMessage instanceof CoapRequest)) {
|
||||
log.info("remoteAddress:{}",coapMessage.getSender());
|
||||
return ;
|
||||
}
|
||||
final CoapRequest coapRequest = (CoapRequest) coapMessage;
|
||||
channelHandlerContext.writeAndFlush(resourceRegistry.respond(coapRequest));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
|
||||
package com.fastbee.coap.handler;
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.fastbee.coap.model.CoapRequest;
|
||||
import com.fastbee.coap.model.CoapResponse;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public interface RequestConsumer {
|
||||
public void processCoapRequest(SettableFuture<CoapResponse> responseFuture, CoapRequest coapRequest,
|
||||
InetSocketAddress remoteSocket) throws Exception;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.fastbee.coap.handler;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.CoapResponse;
|
||||
|
||||
public interface ResourceHandler {
|
||||
/**
|
||||
* The path served by this resource handler.
|
||||
*/
|
||||
public String getPath();
|
||||
|
||||
/**
|
||||
* Detailed title for this path (or <code>null</code>). See http://datatracker.ietf.org/doc/rfc6690/
|
||||
*/
|
||||
public String getTitle();
|
||||
|
||||
/**
|
||||
* Interface name of this resource (or <code>null</code>), can be an URL to a WADL file. See
|
||||
* http://datatracker.ietf.org/doc/rfc6690/
|
||||
*/
|
||||
public String getInterface();
|
||||
|
||||
/**
|
||||
* Resource type (or <code>null</code>). See http://datatracker.ietf.org/doc/rfc6690/
|
||||
*/
|
||||
public String getResourceType();
|
||||
|
||||
/**
|
||||
* Generate the response for this request.
|
||||
*
|
||||
* @param request the request to serve
|
||||
* @return the response
|
||||
*/
|
||||
public CoapResponse handle(CoapMessage request);
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.fastbee.coap.handler;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.CoapResponse;
|
||||
import com.fastbee.coap.model.MessageCode;
|
||||
import com.fastbee.coap.model.options.ContentFormat;
|
||||
|
||||
public class TimeResourceHandler extends AbstractResourceHandler{
|
||||
private final String path;
|
||||
|
||||
public TimeResourceHandler(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
@Override
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CoapResponse handle(CoapMessage request) {
|
||||
if (request.getMessageCode() == MessageCode.GET) {
|
||||
CoapResponse coapResponse = new CoapResponse(request.getMessageType(),
|
||||
MessageCode.CONTENT_205);
|
||||
long time = System.currentTimeMillis() % 86400000;
|
||||
long hours = time / 3600000;
|
||||
long remainder = time % 3600000;
|
||||
long minutes = remainder / 60000;
|
||||
long seconds = (remainder % 60000) / 1000;
|
||||
coapResponse.setContent(String.format("The current time is %02d:%02d:%02d", hours, minutes, seconds).getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
|
||||
coapResponse.setSender(request.getSender());
|
||||
coapResponse.setMessageID(request.getMessageID());
|
||||
coapResponse.setToken(request.getToken());
|
||||
return coapResponse;
|
||||
} else {
|
||||
CoapResponse coapResponse = new CoapResponse(request.getMessageType(),
|
||||
MessageCode.METHOD_NOT_ALLOWED_405);
|
||||
String message = "Service does not allow " + request.getMessageCodeName() + " requests.";
|
||||
coapResponse.setContent(message.getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
|
||||
coapResponse.setSender(request.getSender());
|
||||
coapResponse.setMessageID(request.getMessageID());
|
||||
coapResponse.setToken(request.getToken());
|
||||
return coapResponse;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
public enum BlockSize {
|
||||
UNBOUND(-1, 65536),
|
||||
SIZE_16(0, 16),
|
||||
SIZE_32(1, 32),
|
||||
SIZE_64(2, 64),
|
||||
SIZE_128(3, 128),
|
||||
SIZE_256(4, 256),
|
||||
SIZE_512(5, 512),
|
||||
SIZE_1024(6, 1024);
|
||||
|
||||
private final int encodedSize;
|
||||
private final int decodedSize;
|
||||
|
||||
public static final int UNDEFINED = -1;
|
||||
private static final int SZX_MIN = 0;
|
||||
private static final int SZX_MAX = 6;
|
||||
|
||||
BlockSize(int encodedSize, int decodedSize) {
|
||||
this.encodedSize = encodedSize;
|
||||
this.decodedSize = decodedSize;
|
||||
}
|
||||
|
||||
public int getSzx() {
|
||||
return this.encodedSize;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return this.decodedSize;
|
||||
}
|
||||
|
||||
public static int getSize(long szx) {
|
||||
return getBlockSize(szx).getSize();
|
||||
}
|
||||
|
||||
public static boolean isValid(long szx) {
|
||||
return !(szx < SZX_MIN) && !(szx > SZX_MAX);
|
||||
}
|
||||
|
||||
public static long min(long szx1, long szx2) throws IllegalArgumentException {
|
||||
if (szx1 < UNDEFINED || szx1 > SZX_MAX || szx2 < UNDEFINED || szx2 > SZX_MAX) {
|
||||
throw new IllegalArgumentException("SZX value out of allowed range.");
|
||||
} else if (szx1 == BlockSize.UNDEFINED) {
|
||||
return szx2;
|
||||
} else if (szx2 == BlockSize.UNDEFINED) {
|
||||
return szx1;
|
||||
} else {
|
||||
return Math.min(szx1, szx2);
|
||||
}
|
||||
}
|
||||
|
||||
public static BlockSize getBlockSize(long szx) throws IllegalArgumentException{
|
||||
if (szx == BlockSize.UNDEFINED) {
|
||||
return BlockSize.UNBOUND;
|
||||
} else if (szx == 0) {
|
||||
return SIZE_16;
|
||||
} else if (szx == 1) {
|
||||
return SIZE_32;
|
||||
} else if (szx == 2) {
|
||||
return SIZE_64;
|
||||
} else if (szx == 3) {
|
||||
return SIZE_128;
|
||||
} else if (szx == 4) {
|
||||
return SIZE_256;
|
||||
} else if (szx == 5) {
|
||||
return SIZE_512;
|
||||
} else if (szx == 6) {
|
||||
return SIZE_1024;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported SZX value (Block Option): " + szx);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,494 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.SetMultimap;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.fastbee.coap.model.options.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
import static com.fastbee.coap.model.MessageCode.EMPTY;
|
||||
import static com.fastbee.coap.model.MessageType.*;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
public abstract class CoapMessage {
|
||||
|
||||
/**
|
||||
* The CoAP protocol version (1)
|
||||
*/
|
||||
public static final int PROTOCOL_VERSION = 1;
|
||||
|
||||
/**
|
||||
* The default character set for {@link CoapMessage}s (UTF-8)
|
||||
*/
|
||||
public static final Charset CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
* Internal constant to indicate that the message ID was not yet set (-1)
|
||||
*/
|
||||
public static final int UNDEFINED_MESSAGE_ID = -1;
|
||||
|
||||
/**
|
||||
* The maximum length of the byte array that backs the {@link Token} of {@link CoapMessage} (8)
|
||||
*/
|
||||
public static final int MAX_TOKEN_LENGTH = 8;
|
||||
|
||||
private static final String WRONG_OPTION_TYPE = "Option no. %d is no option of type %s";
|
||||
private static final String OPTION_NOT_ALLOWED_WITH_MESSAGE_TYPE = "Option no. %d (%s) is not allowed with " +
|
||||
"message type %s";
|
||||
private static final String OPTION_ALREADY_SET = "Option no. %d is already set and is only allowed once per " +
|
||||
"message";
|
||||
|
||||
private static final String DOES_NOT_ALLOW_CONTENT = "CoAP messages with code %s do not allow payload.";
|
||||
private static final String EXCLUDES = "Already contained option no. %d excludes option no. %d";
|
||||
|
||||
|
||||
private int messageType;
|
||||
private int messageCode;
|
||||
private int messageID;
|
||||
private Token token;
|
||||
InetSocketAddress sender;
|
||||
|
||||
protected SetMultimap<Integer, OptionValue> options;
|
||||
private ByteBuf content;
|
||||
|
||||
protected CoapMessage(int messageType, int messageCode, int messageID, Token token)
|
||||
throws IllegalArgumentException {
|
||||
|
||||
if (!MessageType.isMessageType(messageType))
|
||||
throw new IllegalArgumentException("No. " + messageType + " is not corresponding to any message type.");
|
||||
|
||||
if (!MessageCode.isMessageCode(messageCode))
|
||||
throw new IllegalArgumentException("No. " + messageCode + " is not corresponding to any message code.");
|
||||
|
||||
this.setMessageType(messageType);
|
||||
this.setMessageCode(messageCode);
|
||||
|
||||
log.debug("Set Message Code to {} ({}).", MessageCode.asString(messageCode), messageCode);
|
||||
|
||||
this.setMessageID(messageID);
|
||||
this.setToken(token);
|
||||
|
||||
this.options = Multimaps.newSetMultimap(new TreeMap<>(),
|
||||
LinkedHashSetSupplier.getInstance());
|
||||
|
||||
this.content = ByteBufAllocator.DEFAULT.buffer();
|
||||
|
||||
log.debug("Created CoAP message: {}", this);
|
||||
}
|
||||
|
||||
protected CoapMessage(int messageType, int messageCode) throws IllegalArgumentException {
|
||||
this(messageType, messageCode, UNDEFINED_MESSAGE_ID, new Token(new byte[0]));
|
||||
}
|
||||
|
||||
public static CoapMessage createEmptyReset(int messageID) throws IllegalArgumentException {
|
||||
return new CoapMessage(RST, EMPTY, messageID, new Token(new byte[0])) {};
|
||||
}
|
||||
|
||||
public static CoapMessage createEmptyAcknowledgement(int messageID) throws IllegalArgumentException {
|
||||
return new CoapMessage(ACK, EMPTY, messageID, new Token(new byte[0])) {};
|
||||
}
|
||||
|
||||
public static CoapMessage createPing(int messageID) throws IllegalArgumentException{
|
||||
return new CoapMessage(CON, EMPTY, messageID, new Token(new byte[0])) {};
|
||||
}
|
||||
|
||||
public void setMessageType(int messageType) throws IllegalArgumentException {
|
||||
if (!MessageType.isMessageType(messageType))
|
||||
throw new IllegalArgumentException("Invalid message type (" + messageType +
|
||||
"). Only numbers 0-3 are allowed.");
|
||||
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public boolean isPing() {
|
||||
return this.messageCode == EMPTY && this.messageType == CON;
|
||||
}
|
||||
|
||||
public boolean isRequest() {
|
||||
return MessageCode.isRequest(this.getMessageCode());
|
||||
}
|
||||
|
||||
public boolean isResponse() {
|
||||
return MessageCode.isResponse(this.getMessageCode());
|
||||
}
|
||||
|
||||
public void addOption(int optionNumber, OptionValue optionValue) throws IllegalArgumentException {
|
||||
this.checkOptionPermission(optionNumber);
|
||||
|
||||
for(int containedOption : options.keySet()) {
|
||||
if (Option.mutuallyExcludes(containedOption, optionNumber))
|
||||
throw new IllegalArgumentException(String.format(EXCLUDES, containedOption, optionNumber));
|
||||
}
|
||||
|
||||
options.put(optionNumber, optionValue);
|
||||
|
||||
log.debug("Added option (number: {}, value: {})", optionNumber, optionValue.toString());
|
||||
|
||||
}
|
||||
|
||||
protected void addStringOption(int optionNumber, String value) throws IllegalArgumentException {
|
||||
|
||||
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.STRING))
|
||||
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.STRING));
|
||||
|
||||
//Add new option to option list
|
||||
StringOptionValue option = new StringOptionValue(optionNumber, value);
|
||||
addOption(optionNumber, option);
|
||||
}
|
||||
|
||||
protected void addUintOption(int optionNumber, long value) throws IllegalArgumentException {
|
||||
|
||||
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.UINT))
|
||||
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.STRING));
|
||||
|
||||
//Add new option to option list
|
||||
byte[] byteValue = Longs.toByteArray(value);
|
||||
int index = 0;
|
||||
while(index < byteValue.length && byteValue[index] == 0) {
|
||||
index++;
|
||||
}
|
||||
UintOptionValue option = new UintOptionValue(optionNumber, Arrays.copyOfRange(byteValue, index, byteValue.length));
|
||||
addOption(optionNumber, option);
|
||||
|
||||
}
|
||||
|
||||
protected void addOpaqueOption(int optionNumber, byte[] value) throws IllegalArgumentException {
|
||||
|
||||
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.OPAQUE))
|
||||
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.OPAQUE));
|
||||
|
||||
//Add new option to option list
|
||||
OpaqueOptionValue option = new OpaqueOptionValue(optionNumber, value);
|
||||
addOption(optionNumber, option);
|
||||
|
||||
}
|
||||
|
||||
protected void addEmptyOption(int optionNumber) throws IllegalArgumentException {
|
||||
|
||||
if (!(OptionValue.getType(optionNumber) == OptionValue.Type.EMPTY))
|
||||
throw new IllegalArgumentException(String.format(WRONG_OPTION_TYPE, optionNumber, OptionValue.Type.EMPTY));
|
||||
|
||||
//Add new option to option list
|
||||
options.put(optionNumber, new EmptyOptionValue(optionNumber));
|
||||
|
||||
log.debug("Added empty option (number: {})", optionNumber);
|
||||
}
|
||||
|
||||
public void removeOptions(int optionNumber) {
|
||||
int result = options.removeAll(optionNumber).size();
|
||||
log.debug("Removed {} options with number {}.", result, optionNumber);
|
||||
}
|
||||
|
||||
private void checkOptionPermission(int optionNumber) throws IllegalArgumentException {
|
||||
Option.Occurence permittedOccurence = Option.getPermittedOccurrence(optionNumber, this.messageCode);
|
||||
if (permittedOccurence == Option.Occurence.NONE) {
|
||||
throw new IllegalArgumentException(String.format(OPTION_NOT_ALLOWED_WITH_MESSAGE_TYPE,
|
||||
optionNumber, Option.asString(optionNumber), this.getMessageCodeName()));
|
||||
} else if (options.containsKey(optionNumber) && permittedOccurence == Option.Occurence.ONCE) {
|
||||
throw new IllegalArgumentException(String.format(OPTION_ALREADY_SET, optionNumber));
|
||||
}
|
||||
}
|
||||
|
||||
private static long extractBits(final long value, final int bits, final int offset) {
|
||||
final long shifted = value >>> offset;
|
||||
final long masked = (1L << bits) - 1L;
|
||||
return shifted & masked;
|
||||
}
|
||||
|
||||
|
||||
public int getProtocolVersion() {
|
||||
return PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
public String getMessageTypeName() {
|
||||
return MessageType.asString(this.messageType);
|
||||
}
|
||||
|
||||
public String getMessageCodeName() {
|
||||
return MessageCode.asString(this.messageCode);
|
||||
}
|
||||
|
||||
public long getContentFormat() {
|
||||
if (options.containsKey(Option.CONTENT_FORMAT)) {
|
||||
return ((UintOptionValue) options.get(Option.CONTENT_FORMAT).iterator().next()).getDecodedValue();
|
||||
} else {
|
||||
return ContentFormat.UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
public void setObserve(long value) {
|
||||
try {
|
||||
this.removeOptions(Option.OBSERVE);
|
||||
value = value & 0xFFFFFF;
|
||||
this.addUintOption(Option.OBSERVE, value);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
this.removeOptions(Option.OBSERVE);
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getObserve() {
|
||||
if (!options.containsKey(Option.OBSERVE)) {
|
||||
return UintOptionValue.UNDEFINED;
|
||||
} else {
|
||||
return (long) options.get(Option.OBSERVE).iterator().next().getDecodedValue();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize2(long size2) throws IllegalArgumentException{
|
||||
this.options.removeAll(Option.SIZE_2);
|
||||
this.addUintOption(Option.SIZE_2, size2);
|
||||
}
|
||||
|
||||
public long getSize2() {
|
||||
if (options.containsKey(Option.SIZE_2)) {
|
||||
return ((UintOptionValue) options.get(Option.SIZE_2).iterator().next()).getDecodedValue();
|
||||
} else {
|
||||
return UintOptionValue.UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize1(long size1) throws IllegalArgumentException{
|
||||
this.options.removeAll(Option.SIZE_1);
|
||||
this.addUintOption(Option.SIZE_1, size1);
|
||||
}
|
||||
|
||||
public long getSize1() {
|
||||
if (options.containsKey(Option.SIZE_1)) {
|
||||
return ((UintOptionValue) options.get(Option.SIZE_1).iterator().next()).getDecodedValue();
|
||||
} else {
|
||||
return UintOptionValue.UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getEndpointID1() {
|
||||
Set<OptionValue> values = getOptions(Option.ENDPOINT_ID_1);
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return values.iterator().next().getValue();
|
||||
}
|
||||
}
|
||||
|
||||
public void setEndpointID1() {
|
||||
this.setEndpointID1(new byte[0]);
|
||||
}
|
||||
|
||||
public void setEndpointID1(byte[] value) {
|
||||
try {
|
||||
this.removeOptions(Option.ENDPOINT_ID_1);
|
||||
this.addOpaqueOption(Option.ENDPOINT_ID_1, value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.removeOptions(Option.ENDPOINT_ID_1);
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getEndpointID2() {
|
||||
Set<OptionValue> values = getOptions(Option.ENDPOINT_ID_2);
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return values.iterator().next().getValue();
|
||||
}
|
||||
}
|
||||
|
||||
public void setEndpointID2(byte[] value) {
|
||||
try {
|
||||
this.removeOptions(Option.ENDPOINT_ID_2);
|
||||
this.addOpaqueOption(Option.ENDPOINT_ID_2, value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.removeOptions(Option.ENDPOINT_ID_2);
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setContent(ByteBuf content) throws IllegalArgumentException {
|
||||
|
||||
if (!(MessageCode.allowsContent(this.messageCode)) && content.readableBytes() > 0) {
|
||||
throw new IllegalArgumentException(String.format(DOES_NOT_ALLOW_CONTENT, this.getMessageCodeName()));
|
||||
}
|
||||
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public void setContent(ByteBuf content, long contentFormat) throws IllegalArgumentException {
|
||||
|
||||
try {
|
||||
this.addUintOption(Option.CONTENT_FORMAT, contentFormat);
|
||||
setContent(content);
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.content = ByteBufAllocator.DEFAULT.buffer();
|
||||
this.removeOptions(Option.CONTENT_FORMAT);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void setContent(byte[] content) throws IllegalArgumentException {
|
||||
setContent(Unpooled.wrappedBuffer(content));
|
||||
}
|
||||
|
||||
public void setContent(byte[] content, long contentFormat) throws IllegalArgumentException {
|
||||
setContent(Unpooled.wrappedBuffer(content), contentFormat);
|
||||
}
|
||||
|
||||
public ByteBuf getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
public byte[] getContentAsByteArray() {
|
||||
byte[] result = new byte[this.getContentLength()];
|
||||
this.getContent().readBytes(result, 0, this.getContentLength());
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getContentLength() {
|
||||
return this.content.readableBytes();
|
||||
}
|
||||
|
||||
public SetMultimap<Integer, OptionValue> getAllOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public void setAllOptions (SetMultimap<Integer, OptionValue> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public Set<OptionValue> getOptions(int optionNumber) {
|
||||
return this.options.get(optionNumber);
|
||||
}
|
||||
|
||||
public boolean containsOption(int optionNumber) {
|
||||
return !getOptions(optionNumber).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode() + content.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
|
||||
if (!(object instanceof CoapMessage)) {
|
||||
log.error("Different type");
|
||||
return false;
|
||||
}
|
||||
|
||||
CoapMessage other = (CoapMessage) object;
|
||||
|
||||
//Check header fields
|
||||
if (this.getProtocolVersion() != other.getProtocolVersion())
|
||||
return false;
|
||||
|
||||
if (this.getMessageType() != other.getMessageType())
|
||||
return false;
|
||||
|
||||
if (this.getMessageCode() != other.getMessageCode())
|
||||
return false;
|
||||
|
||||
if (this.getMessageID() != other.getMessageID())
|
||||
return false;
|
||||
|
||||
if (!this.getToken().equals(other.getToken()))
|
||||
return false;
|
||||
|
||||
|
||||
//Iterators iterate over the contained options
|
||||
Iterator<Map.Entry<Integer, OptionValue>> iterator1 = this.getAllOptions().entries().iterator();
|
||||
Iterator<Map.Entry<Integer, OptionValue>> iterator2 = other.getAllOptions().entries().iterator();
|
||||
|
||||
//Check if both CoAP Messages contain the same options in the same order
|
||||
while(iterator1.hasNext()) {
|
||||
|
||||
//Check if iterator2 has no more options while iterator1 has at least one more
|
||||
if (!iterator2.hasNext())
|
||||
return false;
|
||||
|
||||
Map.Entry<Integer, OptionValue> entry1 = iterator1.next();
|
||||
Map.Entry<Integer, OptionValue> entry2 = iterator2.next();
|
||||
|
||||
if (!entry1.getKey().equals(entry2.getKey()))
|
||||
return false;
|
||||
|
||||
if (!entry1.getValue().equals(entry2.getValue()))
|
||||
return false;
|
||||
}
|
||||
|
||||
//Check if iterator2 has at least one more option while iterator1 has no more
|
||||
if (iterator2.hasNext())
|
||||
return false;
|
||||
|
||||
//Check content
|
||||
return this.getContent().equals(other.getContent());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
//Header + Token
|
||||
result.append("[Header: (V) ").append(getProtocolVersion())
|
||||
.append(", (T) ").append(getMessageTypeName())
|
||||
.append(", (TKL) ").append(token.getBytes().length)
|
||||
.append(", (C) ").append(getMessageCodeName())
|
||||
.append(", (ID) ").append(getMessageID())
|
||||
.append(" | (Token) ").append(token).append(" | ");
|
||||
//Options
|
||||
result.append("Options:");
|
||||
for(int optionNumber : getAllOptions().keySet()) {
|
||||
result.append(" (No. ").append(optionNumber).append(") ");
|
||||
Iterator<OptionValue> iterator = this.getOptions(optionNumber).iterator();
|
||||
OptionValue<?> optionValue = iterator.next();
|
||||
result.append(optionValue.toString());
|
||||
while(iterator.hasNext())
|
||||
result.append(" / ").append(iterator.next().toString());
|
||||
}
|
||||
result.append(" | ");
|
||||
|
||||
//Content
|
||||
result.append("Content: ");
|
||||
long payloadLength = getContent().readableBytes();
|
||||
if (payloadLength == 0)
|
||||
result.append("<no content>]");
|
||||
else
|
||||
result.append(getContent().toString(0, Math.min(getContent().readableBytes(), 20), CoapMessage.CHARSET)).append("... ( ").append(payloadLength).append(" bytes)]");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public void setMessageCode(int messageCode) throws IllegalArgumentException {
|
||||
if (!MessageCode.isMessageCode(messageCode))
|
||||
throw new IllegalArgumentException("Invalid message code no. " + messageCode);
|
||||
|
||||
this.messageCode = messageCode;
|
||||
}
|
||||
|
||||
private final static class LinkedHashSetSupplier implements Supplier<LinkedHashSet<OptionValue>> {
|
||||
|
||||
public static LinkedHashSetSupplier instance = new LinkedHashSetSupplier();
|
||||
|
||||
private LinkedHashSetSupplier() {}
|
||||
|
||||
public static LinkedHashSetSupplier getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkedHashSet<OptionValue> get() {
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,322 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.fastbee.coap.model.options.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.fastbee.coap.model.MessageType.CON;
|
||||
import static com.fastbee.coap.model.MessageType.NON;
|
||||
|
||||
@Slf4j
|
||||
public class CoapRequest extends CoapMessage {
|
||||
|
||||
private static final String NO_REQUEST_TYPE = "Message type %d is not a suitable type for requests (only CON and NON)!";
|
||||
private static final String NO_REQUEST_CODE = "Message code %d is not a request code!";
|
||||
private static final String URI_SCHEME = "URI scheme must be set to \"coap\" (but given URI is: %s)!";
|
||||
private static final String URI_FRAGMENT = "URI must not have a fragment (but given URI is: %s)!";
|
||||
|
||||
public CoapRequest(int messageType, int messageCode, URI targetUri) throws IllegalArgumentException {
|
||||
this(messageType, messageCode, targetUri, false);
|
||||
}
|
||||
|
||||
public CoapRequest(int messageType, int messageCode, URI targetUri, boolean useProxy)
|
||||
throws IllegalArgumentException {
|
||||
|
||||
this(messageType, messageCode);
|
||||
|
||||
if (useProxy) {
|
||||
setProxyURIOption(targetUri);
|
||||
} else {
|
||||
setTargetUriOptions(targetUri);
|
||||
}
|
||||
|
||||
log.debug("New request created: {}.", this);
|
||||
}
|
||||
|
||||
public CoapRequest(int messageType, int messageCode) throws IllegalArgumentException {
|
||||
super(messageType, messageCode);
|
||||
|
||||
if (messageType < CON || messageType > NON) {
|
||||
throw new IllegalArgumentException(String.format(NO_REQUEST_TYPE, messageType));
|
||||
}
|
||||
|
||||
if (!MessageCode.isRequest(messageCode)) {
|
||||
throw new IllegalArgumentException(String.format(NO_REQUEST_CODE, messageCode));
|
||||
}
|
||||
}
|
||||
|
||||
private void setProxyURIOption(URI targetUri) throws IllegalArgumentException {
|
||||
this.addStringOption(Option.PROXY_URI, targetUri.toString());
|
||||
}
|
||||
|
||||
private void setTargetUriOptions(URI targetUri) throws IllegalArgumentException {
|
||||
targetUri = targetUri.normalize();
|
||||
|
||||
//URI must be absolute and thus contain a scheme part (must be one of "coap" or "coaps")
|
||||
String scheme = targetUri.getScheme();
|
||||
if (scheme == null) {
|
||||
throw new IllegalArgumentException(String.format(URI_SCHEME, targetUri.toString()));
|
||||
}
|
||||
|
||||
scheme = scheme.toLowerCase(Locale.ENGLISH);
|
||||
if (!(scheme.equals("coap"))) {
|
||||
throw new IllegalArgumentException(String.format(URI_SCHEME, targetUri.toString()));
|
||||
}
|
||||
|
||||
//Target URI must not have fragment part
|
||||
if (targetUri.getFragment() != null) {
|
||||
throw new IllegalArgumentException(String.format(URI_FRAGMENT, targetUri.toString()));
|
||||
}
|
||||
|
||||
//Create target URI options
|
||||
if (!(OptionValue.isDefaultValue(Option.URI_HOST, targetUri.getHost().getBytes(CoapMessage.CHARSET)))) {
|
||||
addUriHostOption(targetUri.getHost());
|
||||
}
|
||||
|
||||
if (targetUri.getPort() != -1 && targetUri.getPort() != OptionValue.URI_PORT_DEFAULT) {
|
||||
addUriPortOption(targetUri.getPort());
|
||||
}
|
||||
|
||||
addUriPathOptions(targetUri.getPath());
|
||||
addUriQueryOptions(targetUri.getQuery());
|
||||
}
|
||||
|
||||
private void addUriQueryOptions(String uriQuery) throws IllegalArgumentException {
|
||||
if (uriQuery != null) {
|
||||
for(String queryComponent : uriQuery.split("&")) {
|
||||
this.addStringOption(Option.URI_QUERY, queryComponent);
|
||||
log.debug("Added URI query option for {}", queryComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addUriPathOptions(String uriPath) throws IllegalArgumentException {
|
||||
if (uriPath != null) {
|
||||
//Path must not start with "/" to be further processed
|
||||
if (uriPath.startsWith("/")) {
|
||||
uriPath = uriPath.substring(1);
|
||||
}
|
||||
|
||||
if ("".equals(uriPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(String pathComponent : uriPath.split("/")) {
|
||||
this.addStringOption(Option.URI_PATH, pathComponent);
|
||||
log.debug("Added URI path option for {}", pathComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addUriPortOption(int uriPort) throws IllegalArgumentException {
|
||||
if (uriPort > 0 && uriPort != OptionValue.URI_PORT_DEFAULT) {
|
||||
this.addUintOption(Option.URI_PORT, uriPort);
|
||||
}
|
||||
}
|
||||
|
||||
private void addUriHostOption(String uriHost) throws IllegalArgumentException {
|
||||
addStringOption(Option.URI_HOST, uriHost);
|
||||
}
|
||||
|
||||
public void setIfMatch(byte[]... etags) throws IllegalArgumentException {
|
||||
setOpaqueOptions(Option.IF_MATCH, etags);
|
||||
}
|
||||
|
||||
public Set<byte[]> getIfMatch() {
|
||||
|
||||
Set<OptionValue> ifMatchOptionValues = options.get(Option.IF_MATCH);
|
||||
Set<byte[]> result = new HashSet<>(ifMatchOptionValues.size());
|
||||
|
||||
for (OptionValue ifMatchOptionValue : ifMatchOptionValues)
|
||||
result.add(((OpaqueOptionValue) ifMatchOptionValue).getDecodedValue());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getUriHost() {
|
||||
|
||||
if (options.containsKey(Option.URI_HOST))
|
||||
return ((StringOptionValue) options.get(Option.URI_HOST).iterator().next()).getDecodedValue();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setEtags(byte[]... etags) throws IllegalArgumentException {
|
||||
setOpaqueOptions(Option.ETAG, etags);
|
||||
}
|
||||
|
||||
private void setOpaqueOptions(int optionNumber, byte[]... etags) throws IllegalArgumentException {
|
||||
this.removeOptions(optionNumber);
|
||||
try{
|
||||
for(byte[] etag : etags) {
|
||||
this.addOpaqueOption(optionNumber, etag);
|
||||
}
|
||||
} catch(IllegalArgumentException e) {
|
||||
this.removeOptions(optionNumber);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public Set<byte[]> getEtags() {
|
||||
Set<byte[]> result = new HashSet<>();
|
||||
|
||||
for (OptionValue optionValue : options.get(Option.ETAG))
|
||||
result.add(((OpaqueOptionValue) optionValue).getDecodedValue());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean setIfNonMatch() {
|
||||
if (options.containsKey(Option.IF_NONE_MATCH))
|
||||
return true;
|
||||
|
||||
try{
|
||||
this.addEmptyOption(Option.IF_NONE_MATCH);
|
||||
return true;
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isIfNonMatchSet() {
|
||||
return options.containsKey(Option.IF_NONE_MATCH);
|
||||
}
|
||||
|
||||
public long getUriPort() {
|
||||
if (options.containsKey(Option.URI_PORT))
|
||||
return ((UintOptionValue) options.get(Option.URI_PORT).iterator().next()).getDecodedValue();
|
||||
|
||||
return OptionValue.URI_PORT_DEFAULT;
|
||||
}
|
||||
|
||||
public String getUriPath() {
|
||||
String result = "/";
|
||||
|
||||
Iterator<OptionValue> iterator = options.get(Option.URI_PATH).iterator();
|
||||
if (iterator.hasNext())
|
||||
result += ((StringOptionValue) iterator.next()).getDecodedValue();
|
||||
|
||||
while(iterator.hasNext())
|
||||
result += ("/" + ((StringOptionValue) iterator.next()).getDecodedValue());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getUriQuery() {
|
||||
String result = "";
|
||||
|
||||
if (options.containsKey(Option.URI_QUERY)) {
|
||||
|
||||
Iterator<OptionValue> iterator = options.get(Option.URI_QUERY).iterator();
|
||||
result += (((StringOptionValue) iterator.next()).getDecodedValue());
|
||||
|
||||
while(iterator.hasNext())
|
||||
result += ("&" + ((StringOptionValue) iterator.next()).getDecodedValue());
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getUriQueryParameterValue(String parameter) {
|
||||
if (!parameter.endsWith("="))
|
||||
parameter += "=";
|
||||
|
||||
for(OptionValue optionValue : options.get(Option.URI_QUERY)) {
|
||||
String value = ((StringOptionValue) optionValue).getDecodedValue();
|
||||
|
||||
if (value.startsWith(parameter))
|
||||
return value.substring(parameter.length());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setAccept(long... contentFormatNumbers) throws IllegalArgumentException {
|
||||
options.removeAll(Option.ACCEPT);
|
||||
try{
|
||||
for(long contentFormatNumber : contentFormatNumbers)
|
||||
this.addUintOption(Option.ACCEPT, contentFormatNumber);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
options.removeAll(Option.ACCEPT);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Long> getAcceptedContentFormats() {
|
||||
Set<Long> result = new HashSet<>();
|
||||
|
||||
for(OptionValue optionValue : options.get(Option.ACCEPT))
|
||||
result.add(((UintOptionValue) optionValue).getDecodedValue());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public URI getProxyURI() throws URISyntaxException {
|
||||
if (options.containsKey(Option.PROXY_URI)) {
|
||||
OptionValue proxyUriOptionValue = options.get(Option.PROXY_URI).iterator().next();
|
||||
return new URI(((StringOptionValue) proxyUriOptionValue).getDecodedValue());
|
||||
}
|
||||
|
||||
if (options.get(Option.PROXY_SCHEME).size() == 1) {
|
||||
OptionValue proxySchemeOptionValue = options.get(Option.PROXY_SCHEME).iterator().next();
|
||||
String scheme = ((StringOptionValue) proxySchemeOptionValue).getDecodedValue();
|
||||
String uriHost = getUriHost();
|
||||
OptionValue uriPortOptionValue = options.get(Option.URI_PORT).iterator().next();
|
||||
int uriPort = ((UintOptionValue) uriPortOptionValue).getDecodedValue().intValue();
|
||||
String uriPath = getUriPath();
|
||||
String uriQuery = getUriQuery();
|
||||
|
||||
return new URI(scheme, null, uriHost, uriPort == OptionValue.URI_PORT_DEFAULT ? -1 : uriPort, uriPath,
|
||||
uriQuery, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setPreferredBlock2Size(BlockSize size) {
|
||||
this.setBlock2(0, size.getSzx());
|
||||
}
|
||||
|
||||
public void setBlock2(long number, long szx) throws IllegalArgumentException{
|
||||
try {
|
||||
this.removeOptions(Option.BLOCK_2);
|
||||
if (number > 1048575 || !(BlockSize.isValid(szx))) {
|
||||
String error = "Invalid value for BLOCK2 option (NUM: " + number + ", SZX: " + szx + ")";
|
||||
throw new IllegalArgumentException(error);
|
||||
}
|
||||
this.addUintOption(Option.BLOCK_2, ((number & 0xFFFFF) << 4) + szx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPreferredBlock1Size(BlockSize size) {
|
||||
this.setBlock1(0, false, size.getSzx());
|
||||
}
|
||||
|
||||
public void setBlock1(long number, boolean more, long szx) throws IllegalArgumentException{
|
||||
try {
|
||||
this.removeOptions(Option.BLOCK_1);
|
||||
if (number > 1048575 || !(BlockSize.isValid(szx))) {
|
||||
String error = "Invalid value for BLOCK1 option (NUM: " + number + ", SZX: " + szx + ")";
|
||||
throw new IllegalArgumentException(error);
|
||||
}
|
||||
this.addUintOption(Option.BLOCK_1, ((number & 0xFFFFF) << 4) + ((more ? 1 : 0) << 3) + szx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isObservationRequest() {
|
||||
return(!options.get(Option.OBSERVE).isEmpty());
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.fastbee.coap.model.options.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Iterator;
|
||||
|
||||
@Slf4j
|
||||
public class CoapResponse extends CoapMessage {
|
||||
|
||||
private static final String NO_ERRROR_CODE = "Code no. %s is no error code!";
|
||||
|
||||
public CoapResponse(int messageType, int messageCode) throws IllegalArgumentException {
|
||||
super(messageType, messageCode);
|
||||
|
||||
if (!MessageCode.isResponse(messageCode))
|
||||
throw new IllegalArgumentException("Message code no." + messageCode + " is no response code.");
|
||||
}
|
||||
|
||||
public static CoapResponse createErrorResponse(int messageType, int messageCode, String content)
|
||||
throws IllegalArgumentException{
|
||||
|
||||
if (!MessageCode.isErrorMessage(messageCode)) {
|
||||
throw new IllegalArgumentException(String.format(NO_ERRROR_CODE, MessageCode.asString(messageCode)));
|
||||
}
|
||||
|
||||
CoapResponse errorResponse = new CoapResponse(messageType, messageCode);
|
||||
errorResponse.setContent(content.getBytes(CoapMessage.CHARSET), ContentFormat.TEXT_PLAIN_UTF8);
|
||||
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
public static CoapResponse createErrorResponse(int messageType, int messageCode, Throwable throwable)
|
||||
throws IllegalArgumentException{
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(stringWriter));
|
||||
return createErrorResponse(messageType, messageCode, stringWriter.toString());
|
||||
}
|
||||
|
||||
|
||||
public boolean isErrorResponse() {
|
||||
return MessageCode.isErrorMessage(this.getMessageCode());
|
||||
}
|
||||
|
||||
|
||||
public void setEtag(byte[] etag) throws IllegalArgumentException {
|
||||
this.addOpaqueOption(Option.ETAG, etag);
|
||||
}
|
||||
|
||||
public byte[] getEtag() {
|
||||
if (options.containsKey(Option.ETAG)) {
|
||||
return ((OpaqueOptionValue) options.get(Option.ETAG).iterator().next()).getDecodedValue();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setObserve() {
|
||||
this.setObserve(System.currentTimeMillis() % ResourceStatusAge.MODULUS);
|
||||
}
|
||||
|
||||
|
||||
public void setPreferredBlock2Size(BlockSize block2Size) {
|
||||
if (BlockSize.UNBOUND == block2Size || block2Size == null) {
|
||||
this.removeOptions(Option.BLOCK_2);
|
||||
} else {
|
||||
this.setBlock2(0, false, block2Size.getSzx());
|
||||
}
|
||||
}
|
||||
|
||||
public void setBlock2(long number, boolean more, long szx) throws IllegalArgumentException{
|
||||
try {
|
||||
this.removeOptions(Option.BLOCK_2);
|
||||
if (number > 1048575) {
|
||||
throw new IllegalArgumentException("Max. BLOCK2NUM is 1048575");
|
||||
}
|
||||
//long more = ((more) ? 1 : 0) << 3;
|
||||
this.addUintOption(Option.BLOCK_2, ((number & 0xFFFFF) << 4) + ((more ? 1 : 0) << 3) + szx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.removeOptions(Option.BLOCK_2);
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setBlock1(long number, long szx) throws IllegalArgumentException{
|
||||
try {
|
||||
this.removeOptions(Option.BLOCK_1);
|
||||
if (number > 1048575) {
|
||||
throw new IllegalArgumentException("Max. BLOCK1NUM is 1048575");
|
||||
}
|
||||
//long more = ((more) ? 1 : 0) << 3;
|
||||
this.addUintOption(Option.BLOCK_1, ((number & 0xFFFFF) << 4) + (1 << 3) + szx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
this.removeOptions(Option.BLOCK_1);
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUpdateNotification() {
|
||||
return this.getObserve() != UintOptionValue.UNDEFINED;
|
||||
}
|
||||
|
||||
public void setLocationURI(URI locationURI) throws IllegalArgumentException {
|
||||
|
||||
options.removeAll(Option.LOCATION_PATH);
|
||||
options.removeAll(Option.LOCATION_QUERY);
|
||||
|
||||
String locationPath = locationURI.getRawPath();
|
||||
String locationQuery = locationURI.getRawQuery();
|
||||
|
||||
try{
|
||||
if (locationPath != null) {
|
||||
//Path must not start with "/" to be further processed
|
||||
if (locationPath.startsWith("/"))
|
||||
locationPath = locationPath.substring(1);
|
||||
|
||||
for(String pathComponent : locationPath.split("/"))
|
||||
this.addStringOption(Option.LOCATION_PATH, pathComponent);
|
||||
}
|
||||
|
||||
if (locationQuery != null) {
|
||||
for(String queryComponent : locationQuery.split("&"))
|
||||
this.addStringOption(Option.LOCATION_QUERY, queryComponent);
|
||||
}
|
||||
} catch(IllegalArgumentException ex) {
|
||||
options.removeAll(Option.LOCATION_PATH);
|
||||
options.removeAll(Option.LOCATION_QUERY);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public URI getLocationURI() throws URISyntaxException {
|
||||
|
||||
//Reconstruct path
|
||||
StringBuilder locationPath = new StringBuilder();
|
||||
|
||||
if (options.containsKey(Option.LOCATION_PATH)) {
|
||||
for (OptionValue optionValue : options.get(Option.LOCATION_PATH))
|
||||
locationPath.append("/").append(((StringOptionValue) optionValue).getDecodedValue());
|
||||
}
|
||||
|
||||
//Reconstruct query
|
||||
StringBuilder locationQuery = new StringBuilder();
|
||||
|
||||
if (options.containsKey(Option.LOCATION_QUERY)) {
|
||||
Iterator<OptionValue> queryComponentIterator = options.get(Option.LOCATION_QUERY).iterator();
|
||||
locationQuery.append(((StringOptionValue) queryComponentIterator.next()).getDecodedValue());
|
||||
while(queryComponentIterator.hasNext())
|
||||
locationQuery.append("&")
|
||||
.append(((StringOptionValue) queryComponentIterator.next()).getDecodedValue());
|
||||
}
|
||||
|
||||
if (locationPath.length() == 0 && locationQuery.length() == 0)
|
||||
return null;
|
||||
|
||||
return new URI(null, null, null, (int) UintOptionValue.UNDEFINED, locationPath.toString(),
|
||||
locationQuery.toString(), null);
|
||||
}
|
||||
|
||||
public void setMaxAge(long maxAge) {
|
||||
try {
|
||||
this.options.removeAll(Option.MAX_AGE);
|
||||
this.addUintOption(Option.MAX_AGE, maxAge);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("This should never happen.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public long getMaxAge() {
|
||||
if (options.containsKey(Option.MAX_AGE)) {
|
||||
return ((UintOptionValue) options.get(Option.MAX_AGE).iterator().next()).getDecodedValue();
|
||||
} else {
|
||||
return OptionValue.MAX_AGE_DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public abstract class MessageCode {
|
||||
|
||||
/**
|
||||
* Corresponds to Code 0
|
||||
*/
|
||||
public static final int EMPTY = 0;
|
||||
|
||||
/**
|
||||
* Corresponds to Request Code 1
|
||||
*/
|
||||
public static final int GET = 1;
|
||||
|
||||
/**
|
||||
* Corresponds to Request Code 2
|
||||
*/
|
||||
public static final int POST = 2;
|
||||
|
||||
/**
|
||||
* Corresponds to Request Code 3
|
||||
*/
|
||||
public static final int PUT =3;
|
||||
|
||||
/**
|
||||
* Corresponds to Request Code 4
|
||||
*/
|
||||
public static final int DELETE = 4;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 65
|
||||
*/
|
||||
public static final int CREATED_201 = 65;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 66
|
||||
*/
|
||||
public static final int DELETED_202 = 66;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 67
|
||||
*/
|
||||
public static final int VALID_203 = 67;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 68
|
||||
*/
|
||||
public static final int CHANGED_204 = 68;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 69
|
||||
*/
|
||||
public static final int CONTENT_205 = 69;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 95
|
||||
*/
|
||||
public static final int CONTINUE_231 = 95;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 128
|
||||
*/
|
||||
public static final int BAD_REQUEST_400 = 128;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 129
|
||||
*/
|
||||
public static final int UNAUTHORIZED_401 = 129;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 130
|
||||
*/
|
||||
public static final int BAD_OPTION_402 = 130;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 131
|
||||
*/
|
||||
public static final int FORBIDDEN_403 = 131;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 132
|
||||
*/
|
||||
public static final int NOT_FOUND_404 = 132;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 133
|
||||
*/
|
||||
public static final int METHOD_NOT_ALLOWED_405 = 133;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 134
|
||||
*/
|
||||
public static final int NOT_ACCEPTABLE_406 = 134;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 136
|
||||
*/
|
||||
public static final int REQUEST_ENTITY_INCOMPLETE_408 = 136;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 140
|
||||
*/
|
||||
public static final int PRECONDITION_FAILED_412 = 140;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 141
|
||||
*/
|
||||
public static final int REQUEST_ENTITY_TOO_LARGE_413 = 141;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 143
|
||||
*/
|
||||
public static final int UNSUPPORTED_CONTENT_FORMAT_415 = 143;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 160
|
||||
*/
|
||||
public static final int INTERNAL_SERVER_ERROR_500 = 160;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 161
|
||||
*/
|
||||
public static final int NOT_IMPLEMENTED_501 = 161;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 162
|
||||
*/
|
||||
public static final int BAD_GATEWAY_502 = 162;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 163
|
||||
*/
|
||||
public static final int SERVICE_UNAVAILABLE_503 = 163;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 164
|
||||
*/
|
||||
public static final int GATEWAY_TIMEOUT_504 = 164;
|
||||
|
||||
/**
|
||||
* Corresponds to Response Code 165
|
||||
*/
|
||||
public static final int PROXYING_NOT_SUPPORTED_505 = 165;
|
||||
|
||||
private static final HashMap<Integer, String> MESSAGE_CODES = new HashMap<>();
|
||||
static {
|
||||
MESSAGE_CODES.putAll(ImmutableMap.<Integer, String>builder()
|
||||
.put(EMPTY, "EMPTY (" + EMPTY + ")")
|
||||
.put(GET, "GET (" + GET + ")")
|
||||
.put(POST, "POST (" + POST + ")")
|
||||
.put(PUT, "PUT (" + PUT + ")")
|
||||
.put(DELETE, "DELETE (" + DELETE + ")")
|
||||
.put(CREATED_201, "CREATED (" + CREATED_201 + ")")
|
||||
.put(DELETED_202, "DELETED (" + DELETED_202 + ")")
|
||||
.put(VALID_203, "VALID (" + VALID_203 + ")")
|
||||
.put(CHANGED_204, "CHANGED (" + CHANGED_204 + ")")
|
||||
.put(CONTENT_205, "CONTENT (" + CONTENT_205 + ")")
|
||||
.put(CONTINUE_231, "CONTINUE (" + CONTINUE_231 + ")")
|
||||
.put(BAD_REQUEST_400, "BAD REQUEST (" + BAD_REQUEST_400 + ")")
|
||||
.put(UNAUTHORIZED_401, "UNAUTHORIZED (" + UNAUTHORIZED_401 + ")")
|
||||
.put(BAD_OPTION_402, "BAD OPTION (" + BAD_OPTION_402 + ")")
|
||||
.put(FORBIDDEN_403, "FORBIDDEN (" + FORBIDDEN_403 + ")")
|
||||
.put(NOT_FOUND_404, "NOT FOUND (" + NOT_FOUND_404 + ")")
|
||||
.put(METHOD_NOT_ALLOWED_405, "METHOD NOT ALLOWED (" + METHOD_NOT_ALLOWED_405 + ")")
|
||||
.put(NOT_ACCEPTABLE_406, "NOT ACCEPTABLE (" + NOT_ACCEPTABLE_406 + ")")
|
||||
.put(REQUEST_ENTITY_INCOMPLETE_408, "REQUEST ENTITY INCOMPLETE (" + REQUEST_ENTITY_INCOMPLETE_408 + ")")
|
||||
.put(PRECONDITION_FAILED_412, "PRECONDITION FAILED (" + PRECONDITION_FAILED_412 + ")")
|
||||
.put(REQUEST_ENTITY_TOO_LARGE_413, "REQUEST ENTITY TOO LARGE (" + REQUEST_ENTITY_TOO_LARGE_413 + ")")
|
||||
.put(UNSUPPORTED_CONTENT_FORMAT_415, "UNSUPPORTED CONTENT FORMAT (" + UNSUPPORTED_CONTENT_FORMAT_415 + ")")
|
||||
.put(INTERNAL_SERVER_ERROR_500, "INTERNAL SERVER ERROR (" + INTERNAL_SERVER_ERROR_500 + ")")
|
||||
.put(NOT_IMPLEMENTED_501, "NOT IMPLEMENTED (" + NOT_IMPLEMENTED_501 + ")")
|
||||
.put(BAD_GATEWAY_502, "BAD GATEWAY (" + BAD_GATEWAY_502 + ")")
|
||||
.put(SERVICE_UNAVAILABLE_503, "SERVICE UNAVAILABLE (" + SERVICE_UNAVAILABLE_503 + ")")
|
||||
.put(GATEWAY_TIMEOUT_504, "GATEWAY TIMEOUT (" + GATEWAY_TIMEOUT_504 + ")")
|
||||
.put(PROXYING_NOT_SUPPORTED_505, "PROXYING NOT SUPPORTED (" + PROXYING_NOT_SUPPORTED_505 + ")")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
public static String asString(int messageCode) {
|
||||
String result = MESSAGE_CODES.get(messageCode);
|
||||
return result == null ? "UNKOWN (" + messageCode + ")" : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the given number corresponds to a valid
|
||||
* {@link MessageCode} and <code>false</code> otherwise
|
||||
*
|
||||
* @param number the number to check for being a valid {@link MessageCode}
|
||||
*
|
||||
* @return <code>true</code> if the given number corresponds to a valid
|
||||
* {@link MessageCode} and <code>false</code> otherwise
|
||||
*/
|
||||
public static boolean isMessageCode(int number) {
|
||||
return MESSAGE_CODES.containsKey(number);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapRequest}s.
|
||||
*
|
||||
* <b>Note:</b> Messages with {@link MessageCode#EMPTY} are considered neither a response nor a request
|
||||
*
|
||||
* @return <code>true</code> in case of a request code, <code>false</code> otherwise.
|
||||
*
|
||||
*/
|
||||
public static boolean isRequest(int messageCode) {
|
||||
return (messageCode > 0 && messageCode < 5);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapResponse}s.
|
||||
*
|
||||
* <b>Note:</b> Messages with {@link MessageCode#EMPTY} are considered neither a response nor a request
|
||||
*
|
||||
* @return <code>true</code> in case of a response code, <code>false</code> otherwise.
|
||||
*
|
||||
*/
|
||||
public static boolean isResponse(int messageCode) {
|
||||
return messageCode >= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method indicates whether the given number refers to a {@link MessageCode} for {@link CoapResponse}s
|
||||
* indicating an error.
|
||||
*
|
||||
* @return <code>true</code> in case of an error response code, <code>false</code> otherwise.
|
||||
*
|
||||
*/
|
||||
public static boolean isErrorMessage(int codeNumber) {
|
||||
return (codeNumber >= 128);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method indicates whether a message may contain payload
|
||||
* @return <code>true</code> if payload is allowed, <code>false</code> otherwise
|
||||
*/
|
||||
public static boolean allowsContent(int codeNumber) {
|
||||
return !(codeNumber == GET || codeNumber == DELETE);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public abstract class MessageType {
|
||||
|
||||
/**
|
||||
* Corresponds to Message EventType 0
|
||||
*/
|
||||
public static final int CON = 0;
|
||||
|
||||
/**
|
||||
* Corresponds to Message EventType 1
|
||||
*/
|
||||
public static final int NON = 1;
|
||||
|
||||
/**
|
||||
* Corresponds to Message EventType 2
|
||||
*/
|
||||
public static final int ACK = 2;
|
||||
|
||||
/**
|
||||
* Corresponds to Message EventType 3
|
||||
*/
|
||||
public static final int RST = 3;
|
||||
|
||||
private static final HashMap<Integer, String> MESSAGE_TYPES = new HashMap<>();
|
||||
static {
|
||||
MESSAGE_TYPES.putAll(ImmutableMap.<Integer, String>builder()
|
||||
.put(CON, "CON (" + CON + ")")
|
||||
.put(NON, "NON (" + NON + ")")
|
||||
.put(ACK, "ACK (" + ACK + ")")
|
||||
.put(RST, "RST (" + RST + ")")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static String asString(int messageType) {
|
||||
String result = MESSAGE_TYPES.get(messageType);
|
||||
return result == null ? "UNKOWN (" + messageType + ")" : result;
|
||||
}
|
||||
|
||||
public static boolean isMessageType(int number) {
|
||||
return MESSAGE_TYPES.containsKey(number);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ResourceStatusAge {
|
||||
|
||||
public static final long MODULUS = (long) Math.pow(2, 24);
|
||||
private static final long THRESHOLD = (long) Math.pow(2, 23);
|
||||
|
||||
private final long sequenceNo;
|
||||
private final long timestamp;
|
||||
|
||||
public ResourceStatusAge(long sequenceNo, long timestamp) {
|
||||
this.sequenceNo = sequenceNo;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static boolean isReceivedStatusNewer(ResourceStatusAge latest, ResourceStatusAge received) {
|
||||
if (latest.sequenceNo < received.sequenceNo && received.sequenceNo - latest.sequenceNo < THRESHOLD) {
|
||||
log.debug("Criterion 1 matches: received ({}) is newer than latest ({}).", received, latest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (latest.sequenceNo > received.sequenceNo && latest.sequenceNo - received.sequenceNo > THRESHOLD) {
|
||||
log.debug("Criterion 2 matches: received ({}) is newer than latest ({}).", received, latest);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (received.timestamp > latest.timestamp + 128000L) {
|
||||
log.debug("Criterion 3 matches: received ({}) is newer than latest ({}).", received, latest);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.debug("No criterion matches: received({}) is older than latest ({}).", received, latest);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "STATUS AGE (Sequence No: " + this.sequenceNo + ", Reception Timestamp: " + this.timestamp + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package com.fastbee.coap.model;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.google.common.primitives.UnsignedLongs;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Token implements Comparable<Token>{
|
||||
|
||||
public static int MAX_LENGTH = 8;
|
||||
|
||||
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
private final byte[] token;
|
||||
|
||||
public Token(byte[] token) {
|
||||
if (token.length > 8)
|
||||
throw new IllegalArgumentException("Maximum token length is 8 (but given length was " + token.length + ")");
|
||||
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String tmp = bytesToHex(getBytes());
|
||||
|
||||
if (tmp.length() == 0)
|
||||
return "<EMPTY>";
|
||||
else
|
||||
return "0x" + tmp;
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if ((!(object instanceof Token)))
|
||||
return false;
|
||||
|
||||
Token other = (Token) object;
|
||||
return Arrays.equals(this.getBytes(), other.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Token other) {
|
||||
|
||||
if (other.equals(this))
|
||||
return 0;
|
||||
if (this.getBytes().length < other.getBytes().length)
|
||||
return -1;
|
||||
if (this.getBytes().length > other.getBytes().length)
|
||||
return 1;
|
||||
|
||||
return UnsignedLongs.compare(Longs.fromByteArray(Bytes.concat(this.getBytes(), new byte[8])),
|
||||
Longs.fromByteArray(Bytes.concat(other.getBytes(), new byte[8])));
|
||||
}
|
||||
}
|
@ -0,0 +1,436 @@
|
||||
package com.fastbee.coap.model.linkformat;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.options.StringOptionValue;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public class LinkParam {
|
||||
|
||||
/**
|
||||
* The enumeration {@link Key} contains all link-param-keys that are supported
|
||||
*
|
||||
* @author Oliver Kleine
|
||||
*/
|
||||
public enum Key {
|
||||
/**
|
||||
* Corresponds to link-param-key "rel"
|
||||
*/
|
||||
REL("rel", ValueType.RELATION_TYPE, ValueType.DQUOTED_RELATION_TYPES),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "anchor"
|
||||
*/
|
||||
ANCHOR("anchor", ValueType.DQUOTED_URI_REFERENCE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "rev"
|
||||
*/
|
||||
REV("rev", ValueType.RELATION_TYPE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "hreflang"
|
||||
*/
|
||||
HREFLANG("hreflang", ValueType.LANGUAGE_TAG),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "media"
|
||||
*/
|
||||
MEDIA("media", ValueType.MEDIA_DESC, ValueType.DQUOTED_MEDIA_DESC),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "title"
|
||||
*/
|
||||
TITLE("title", ValueType.DQUOTED_STRING),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "title*"
|
||||
*/
|
||||
TITLE_STAR("title*", ValueType.EXT_VALUE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "type"
|
||||
*/
|
||||
TYPE("type", ValueType.MEDIA_TYPE, ValueType.DQUOTED_MEDIA_TYPE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "rt"
|
||||
*/
|
||||
RT("rt", ValueType.RELATION_TYPE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "if"
|
||||
*/
|
||||
IF("if", ValueType.RELATION_TYPE),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "sz"
|
||||
*/
|
||||
SZ("sz", ValueType.CARDINAL),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "ct"
|
||||
*/
|
||||
CT("ct", ValueType.CARDINAL, ValueType.DQUOTED_CARDINALS),
|
||||
|
||||
/**
|
||||
* Corresponds to link-param-key "obs"
|
||||
*/
|
||||
OBS("obs", ValueType.EMPTY),
|
||||
|
||||
/**
|
||||
* Used internally for unknown link-param-keys
|
||||
*/
|
||||
UNKNOWN(null, ValueType.UNKNOWN);
|
||||
|
||||
private final String keyName;
|
||||
private final Set<ValueType> valueTypes;
|
||||
|
||||
Key(String keyName, ValueType... valueType) {
|
||||
this.keyName = keyName;
|
||||
this.valueTypes = new HashSet<>(valueType.length);
|
||||
this.valueTypes.addAll(Arrays.asList(valueType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this link-param-key (i.e. "ct")
|
||||
* @return the name of this link-param-key (i.e. "ct")
|
||||
*/
|
||||
public String getKeyName() {
|
||||
return this.keyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ValueType}s that are allowed for values of this key
|
||||
* @return the {@link ValueType}s that are allowed for values of this key
|
||||
*/
|
||||
public Set<ValueType> getValueTypes() {
|
||||
return this.valueTypes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The enumeration {@link ValueType} contains all value types that are supported
|
||||
*
|
||||
* @author Oliver Kleine
|
||||
*/
|
||||
public enum ValueType {
|
||||
|
||||
/**
|
||||
* Corresponds to the empty type, i.e. no value
|
||||
*/
|
||||
EMPTY (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "relation-types"
|
||||
*/
|
||||
RELATION_TYPE (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to one or more values of type "relation-types" enclosed in double-quotes (<code>DQUOTE</code>)
|
||||
*/
|
||||
DQUOTED_RELATION_TYPES(true, true),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "URI reference"
|
||||
*/
|
||||
DQUOTED_URI_REFERENCE(true, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "Language-Tag"
|
||||
*/
|
||||
LANGUAGE_TAG (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "Media Desc"
|
||||
*/
|
||||
MEDIA_DESC (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "Media Desc" enclosed in double-quotes (<code>DQUOTE</code>)
|
||||
*/
|
||||
DQUOTED_MEDIA_DESC(true, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "quoted-string", i.e. a string value enclosed in double-quotes
|
||||
* (<code>DQUOTE</code>)
|
||||
*/
|
||||
DQUOTED_STRING(true, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "ext-value"
|
||||
*/
|
||||
EXT_VALUE (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "media-type"
|
||||
*/
|
||||
MEDIA_TYPE (false, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "media-type" enclosed in double-quotes (<code>DQUOTE</code>)
|
||||
*/
|
||||
DQUOTED_MEDIA_TYPE(true, false),
|
||||
|
||||
/**
|
||||
* Corresponds to a single value of type "cardinal", i.e. digits
|
||||
*/
|
||||
CARDINAL (false, false),
|
||||
|
||||
/**
|
||||
* Values of this type consist of multiple cardinal values, divided by white spaces and enclosed in
|
||||
* double-quotes (<code>DQUOTE</code>)
|
||||
*/
|
||||
DQUOTED_CARDINALS(true, true),
|
||||
|
||||
/**
|
||||
* Internally used to represent all other types
|
||||
*/
|
||||
UNKNOWN(false, false);
|
||||
|
||||
private final boolean doubleQuoted;
|
||||
private final boolean multipleValues;
|
||||
|
||||
ValueType(boolean doubleQuoted, boolean multipleValues) {
|
||||
this.doubleQuoted = doubleQuoted;
|
||||
this.multipleValues = multipleValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if this {@link ValueType} allows multiple values divided by white spaces and
|
||||
* <code>false</code> otherwise
|
||||
*
|
||||
* @return <code>true</code> if this {@link ValueType} allows multiple values divided by white spaces and
|
||||
* <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isMultipleValues() {
|
||||
return this.multipleValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if values of this {@link ValueType} are enclosed in double-quotes
|
||||
* (<code>DQUOTE</code>) and <code>false</code> otherwise
|
||||
*
|
||||
* @return <code>true</code> if values of this {@link ValueType} are enclosed in double-quotes
|
||||
* (<code>DQUOTE</code>) and <code>false</code> otherwise
|
||||
*/
|
||||
public boolean isDoubleQuoted() {
|
||||
return this.doubleQuoted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the {@link Key} corresponding to the given name or <code>null</code> if no such {@link Key} exists
|
||||
*
|
||||
* @param keyName the name of the {@link Key} to lookup
|
||||
* @return the {@link Key} corresponding to the given name or <code>null</code> if no such {@link Key} exists
|
||||
*/
|
||||
public static Key getKey(String keyName) {
|
||||
for(Key key : Key.values()) {
|
||||
if (key.getKeyName().equals(keyName)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ValueType} that corresponds to the given key-value-pair or <code>null</code> if no such
|
||||
* {@link ValueType} exists.
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*
|
||||
* @return the {@link ValueType} that corresponds to the given key-value-pair or <code>null</code> if no such
|
||||
* {@link ValueType} exists.
|
||||
*/
|
||||
public static ValueType getValueType(Key key, String value) {
|
||||
// determine possible value types
|
||||
Set<ValueType> valueTypes = key.getValueTypes();
|
||||
|
||||
// check if link param value is quoted and if there is quoted type
|
||||
if (valueTypes.size() == 1) {
|
||||
return valueTypes.iterator().next();
|
||||
} else if (value.startsWith("\"") && value.endsWith("\"")) {
|
||||
for (ValueType valueType : valueTypes) {
|
||||
if(valueType.isDoubleQuoted()) {
|
||||
return valueType;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (ValueType valueType : valueTypes) {
|
||||
if(!valueType.isDoubleQuoted()) {
|
||||
return valueType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given (serialized) link param (e.g. <code>ct=40</code>)
|
||||
* @param linkParam the serialized link param
|
||||
* @return an instance of {@link LinkParam} according to the given parameter
|
||||
*/
|
||||
public static LinkParam decode(String linkParam) {
|
||||
// remove percent encoding
|
||||
byte[] tmp = StringOptionValue.convertToByteArrayWithoutPercentEncoding(linkParam);
|
||||
linkParam = new String(tmp, CoapMessage.CHARSET);
|
||||
|
||||
// determine the key of this link param
|
||||
String keyName = !linkParam.contains("=") ? linkParam : linkParam.substring(0, linkParam.indexOf("="));
|
||||
LinkParam.Key key = LinkParam.getKey(keyName);
|
||||
|
||||
if(key == null) {
|
||||
log.warn("Unsupported key name for link param: {}", keyName);
|
||||
return null;
|
||||
} else if (keyName.equals(linkParam)) {
|
||||
// empty attribute
|
||||
if(!key.getValueTypes().contains(ValueType.EMPTY)) {
|
||||
log.debug("Key {} does not support empty values!", key.getKeyName());
|
||||
return null;
|
||||
} else {
|
||||
return new LinkParam(key, ValueType.EMPTY, null);
|
||||
}
|
||||
} else {
|
||||
// link param has non-empty value
|
||||
String value = linkParam.substring(linkParam.indexOf("=") + 1, linkParam.length());
|
||||
LinkParam.ValueType valueType = LinkParam.getValueType(key, value);
|
||||
|
||||
if(valueType == null) {
|
||||
log.warn("Could not determine value type for key \"{}\" and value\"{}\".", keyName, value);
|
||||
return null;
|
||||
} else {
|
||||
log.debug("Value: {}, Type: {}", value, valueType);
|
||||
return new LinkParam(key, valueType, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Creates a new instance of {@link LinkParam}</p>
|
||||
*
|
||||
* <p><b>Note:</b>For some kinds of link params the enclosing double quotes are part of the value (e.g. value "0 41"
|
||||
* for {@link Key#CT} or "Some title" for {@link Key#TITLE}). Thus, the latter is created using
|
||||
* <code>createLinkParam(Key.TITLE, "\"Some title\"")</code>
|
||||
* </p>
|
||||
*
|
||||
* @param key the {@link Key} of the link param to be created
|
||||
* @param value the value of the link param to be created (see note above)
|
||||
*
|
||||
* @return a new instance of {@link LinkParam} according to the given parameters (key and value)
|
||||
*/
|
||||
public static LinkParam createLinkParam(Key key, String value) {
|
||||
ValueType valueType = getValueType(key, value);
|
||||
if (valueType == null) {
|
||||
log.warn("Could not determine value type for key \"{}\" and value\"{}\".", key.getKeyName(), value);
|
||||
return null;
|
||||
} else {
|
||||
return new LinkParam(key, valueType, value);
|
||||
}
|
||||
}
|
||||
|
||||
//******************************************************************************************
|
||||
// instance related fields and methods
|
||||
//******************************************************************************************
|
||||
|
||||
private final Key key;
|
||||
private final ValueType valueType;
|
||||
private final String value;
|
||||
|
||||
|
||||
private LinkParam(Key key, ValueType valueType, String value) {
|
||||
this.key = key;
|
||||
this.valueType = valueType;
|
||||
// remove double quotes if existing
|
||||
this.value = valueType.isDoubleQuoted() ? value.substring(1, value.length() - 1) : value;
|
||||
log.debug("LinkParam created: {}", this.toString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the {@link Key} of this {@link LinkParam}
|
||||
* @return the {@link Key} of this {@link LinkParam}
|
||||
*/
|
||||
public Key getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for {@link #getKey()#getKeyName()}
|
||||
* @return the name of the {@link Key} of this {@link LinkParam} (e.g. "ct" or "rt")
|
||||
*/
|
||||
public String getKeyName() {
|
||||
return this.key.getKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ValueType} of the value returned by {@link #getValue()}
|
||||
* @return the {@link ValueType} of the value returned by {@link #getValue()}
|
||||
*/
|
||||
public ValueType getValueType() {
|
||||
return this.valueType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of this {@link LinkParam}
|
||||
* @return the value of this {@link LinkParam}
|
||||
*/
|
||||
public String getValue() {
|
||||
if (this.valueType.isDoubleQuoted()) {
|
||||
return "\"" + this.value + "\"";
|
||||
} else {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns <code>true</code> if the given value is contained in the value returned by {@link #getValue()} and
|
||||
* <code>false</code> otherwise. The exact behaviour depends on whether there are multiple values allowed in a
|
||||
* single param (see: {@link ValueType#isMultipleValues()}).</p>
|
||||
*
|
||||
* <p>Example: If the {@link LinkParam} corresponds to <code>ct="0 41"</code> then both, <code>contains("0")</code>
|
||||
* and <code>contains("41")</code> return <code>true</code> but <code>contains("0 41")</code> returns
|
||||
* <code>false</code>.</p>
|
||||
*
|
||||
* @param value the value to check
|
||||
*
|
||||
* @return <code>true</code> if the given value is contained in the value returned by {@link #getValue()} and
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean contains(String value) {
|
||||
if (this.valueType.isMultipleValues()){
|
||||
return Arrays.asList(this.value.split(" ")).contains(value);
|
||||
} else {
|
||||
return this.value.equals(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this {@link LinkParam}
|
||||
* @return a string representation of this {@link LinkParam}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(this.key.getKeyName());
|
||||
if (this.valueType != ValueType.EMPTY) {
|
||||
builder.append("=");
|
||||
if (this.valueType.doubleQuoted) {
|
||||
builder.append("\"");
|
||||
}
|
||||
builder.append(this.value);
|
||||
if (this.valueType.doubleQuoted) {
|
||||
builder.append("\"");
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.fastbee.coap.model.linkformat;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class LinkValue {
|
||||
|
||||
//*******************************************************************************************
|
||||
// static fields and methods
|
||||
//*******************************************************************************************
|
||||
|
||||
static LinkValue decode(String linkValue) {
|
||||
LinkValue result = new LinkValue(getUriReference(linkValue));
|
||||
for (String linkParam : LinkValue.getLinkParams(linkValue)) {
|
||||
result.addLinkParam(LinkParam.decode(linkParam));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getUriReference(String linkValue) {
|
||||
String uriReference = linkValue.substring(linkValue.indexOf("<") + 1, linkValue.indexOf(">"));
|
||||
log.info("Found URI reference <{}>", uriReference);
|
||||
return uriReference;
|
||||
}
|
||||
|
||||
private static List<String> getLinkParams(String linkValue) {
|
||||
String[] linkParams = linkValue.split(";");
|
||||
return new ArrayList<>(Arrays.asList(linkParams).subList(1, linkParams.length));
|
||||
}
|
||||
|
||||
|
||||
//******************************************************************************************
|
||||
// instance related fields and methods
|
||||
//******************************************************************************************
|
||||
|
||||
private final String uriReference;
|
||||
private final Collection<LinkParam> linkParams;
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link LinkValue}
|
||||
* @param uriReference the URI reference, i.e. the resource to be described
|
||||
* @param linkParams the {@link LinkParam}s to describe the resource
|
||||
*/
|
||||
public LinkValue(String uriReference, Collection<LinkParam> linkParams) {
|
||||
this.uriReference = uriReference;
|
||||
this.linkParams = linkParams;
|
||||
}
|
||||
|
||||
private LinkValue(String uriReference) {
|
||||
this(uriReference, new ArrayList<LinkParam>());
|
||||
}
|
||||
|
||||
private void addLinkParam(LinkParam linkParams) {
|
||||
this.linkParams.add(linkParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI reference of this {@link LinkValue}
|
||||
* @return the URI reference of this {@link LinkValue}
|
||||
*/
|
||||
public String getUriReference() {
|
||||
return this.uriReference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link LinkParam}s describing the resource identified by the URI reference
|
||||
* @return the {@link LinkParam}s describing this resource identified by the URI reference
|
||||
*/
|
||||
public Collection<LinkParam> getLinkParams() {
|
||||
return this.linkParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if this {@link LinkValue} contains a {@link LinkParam} that matches the given
|
||||
* criterion, i.e. the given key-value-pair and <code>false</code> otherwise.
|
||||
*
|
||||
* @param key the key of the criterion
|
||||
* @param value the value of the criterion
|
||||
*
|
||||
* @return <code>true</code> if this {@link LinkValue} contains a {@link LinkParam} that matches the given
|
||||
* criterion, i.e. the given key-value-pair and <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean containsLinkParam(LinkParam.Key key, String value) {
|
||||
for (LinkParam linkParam : this.linkParams) {
|
||||
if (key.equals(linkParam.getKey())) {
|
||||
return value == null || linkParam.contains(value);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this {@link LinkValue}.
|
||||
* @return a string representation of this {@link LinkValue}.
|
||||
*/
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("<").append(uriReference).append(">");
|
||||
for (LinkParam linkParam : this.getLinkParams()) {
|
||||
builder.append(";").append(linkParam.toString());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package com.fastbee.coap.model.linkformat;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import com.fastbee.coap.model.options.ContentFormat;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class LinkValueList {
|
||||
|
||||
//*******************************************************************************************
|
||||
// static methods
|
||||
//*******************************************************************************************
|
||||
|
||||
/**
|
||||
* Decodes a serialized link-value-list, e.g. the payload (content) of a {@link CoapMessage} with content type
|
||||
* {@link ContentFormat#APP_LINK_FORMAT} and returns a corresponding {@link LinkValueList} instance.
|
||||
*
|
||||
* @param linkValueList the serialized link-value-list
|
||||
*
|
||||
* @return A {@link LinkValueList} instance corresponsing to the given serialization
|
||||
*/
|
||||
public static LinkValueList decode(String linkValueList) {
|
||||
LinkValueList result = new LinkValueList();
|
||||
Collection<String> linkValues = getLinkValues(linkValueList);
|
||||
for(String linkValue : linkValues) {
|
||||
result.addLinkValue(LinkValue.decode(linkValue));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Collection<String> getLinkValues(String linkValueList) {
|
||||
List<String> linkValues = new ArrayList<>();
|
||||
Collections.addAll(linkValues, linkValueList.split(","));
|
||||
return linkValues;
|
||||
}
|
||||
|
||||
//******************************************************************************************
|
||||
// instance related fields and methods
|
||||
//******************************************************************************************
|
||||
|
||||
private Collection<LinkValue> linkValues;
|
||||
|
||||
private LinkValueList() {
|
||||
this.linkValues = new TreeSet<>(new Comparator<LinkValue>() {
|
||||
@Override
|
||||
public int compare(LinkValue linkValue1, LinkValue linkValue2) {
|
||||
return linkValue1.getUriReference().compareTo(linkValue2.getUriReference());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link LinkValueList}
|
||||
* @param linkValues the {@link LinkValue}s to be contained in the {@link LinkValueList} to be created
|
||||
*/
|
||||
public LinkValueList(LinkValue... linkValues) {
|
||||
this.linkValues = new ArrayList<>(Arrays.asList(linkValues));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an instance of {@link LinkValue} to this {@link LinkValueList}.
|
||||
* @param linkValue the {@link LinkValue} to be added
|
||||
*/
|
||||
public void addLinkValue(LinkValue linkValue) {
|
||||
this.linkValues.add(linkValue);
|
||||
}
|
||||
|
||||
public boolean removeLinkValue(String uriReference) {
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
if (linkValue.getUriReference().equals(uriReference)) {
|
||||
this.linkValues.remove(linkValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all URI references contained in this {@link LinkValueList}
|
||||
*
|
||||
* @return all URI references contained in this {@link LinkValueList}
|
||||
*/
|
||||
public List<String> getUriReferences() {
|
||||
List<String> result = new ArrayList<>(linkValues.size());
|
||||
for (LinkValue linkValue : linkValues) {
|
||||
result.add(linkValue.getUriReference());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI references that match the given criterion, i.e. contain a {@link LinkParam} with the given
|
||||
* pair of keyname and value.
|
||||
*
|
||||
* @param key the {@link LinkParam.Key} to match
|
||||
* @param value the value to match
|
||||
*
|
||||
* @return the URI references that match the given criterion
|
||||
*/
|
||||
public Set<String> getUriReferences(LinkParam.Key key, String value) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (LinkValue linkValue : linkValues) {
|
||||
if (linkValue.containsLinkParam(key, value)) {
|
||||
result.add(linkValue.getUriReference());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link LinkParam}s for the given URI reference.
|
||||
*
|
||||
* @param uriReference the URI reference to lookup the {@link LinkParam}s for
|
||||
*
|
||||
* @return the {@link LinkParam}s for the given URI reference
|
||||
*/
|
||||
public Collection<LinkParam> getLinkParams(String uriReference) {
|
||||
List<LinkParam> result = new ArrayList<>();
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
if (linkValue.getUriReference().equals(uriReference)) {
|
||||
return linkValue.getLinkParams();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public LinkValueList filter(LinkParam.Key key, String value) {
|
||||
LinkValueList result = new LinkValueList();
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
if (linkValue.containsLinkParam(key, value)) {
|
||||
result.addLinkValue(linkValue);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public LinkValueList filter(String hrefValue) {
|
||||
if (hrefValue.endsWith("*")) {
|
||||
return filterByUriPrefix(hrefValue.substring(0, hrefValue.length() - 1));
|
||||
} else {
|
||||
return filterByUriReference(hrefValue);
|
||||
}
|
||||
}
|
||||
|
||||
private LinkValueList filterByUriPrefix(String prefix) {
|
||||
LinkValueList result = new LinkValueList();
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
if (linkValue.getUriReference().startsWith(prefix)) {
|
||||
result.addLinkValue(linkValue);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private LinkValueList filterByUriReference(String uriReference) {
|
||||
LinkValueList result = new LinkValueList();
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
if (linkValue.getUriReference().endsWith(uriReference)) {
|
||||
result.addLinkValue(linkValue);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Returns a string representation of this {@link LinkValueList}, i.e. the reversal of {@link #decode(String)}
|
||||
* @return a string representation of this {@link LinkValueList}, i.e. the reversal of {@link #decode(String)}
|
||||
*/
|
||||
public String encode() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (LinkValue linkValue : this.linkValues) {
|
||||
builder.append(linkValue.toString());
|
||||
builder.append(",");
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.deleteCharAt(builder.length() - 1);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this {@link LinkValueList} (same as {@link #encode()}
|
||||
* @return a string representation of this {@link LinkValueList} (same as {@link #encode()}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.encode();
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
public abstract class ContentFormat {
|
||||
|
||||
/**
|
||||
* Corresponds to number -1
|
||||
*/
|
||||
public static final long UNDEFINED = -1;
|
||||
|
||||
/**
|
||||
* Corresponds to number 0
|
||||
*/
|
||||
public static final long TEXT_PLAIN_UTF8 = 0;
|
||||
|
||||
/**
|
||||
* Corresponds to number 40
|
||||
*/
|
||||
public static final long APP_LINK_FORMAT = 40;
|
||||
|
||||
/**
|
||||
* Corresponds to number 41
|
||||
*/
|
||||
public static final long APP_XML = 41;
|
||||
|
||||
/**
|
||||
* Corresponds to number 42
|
||||
*/
|
||||
public static final long APP_OCTET_STREAM = 42;
|
||||
|
||||
/**
|
||||
* Corresponds to number 47
|
||||
*/
|
||||
public static final long APP_EXI = 47;
|
||||
|
||||
/**
|
||||
* Corresponds to number 50
|
||||
*/
|
||||
public static final long APP_JSON = 50;
|
||||
|
||||
/**
|
||||
* Corresponds to number 201 (no standard but defined for very selfish reasons)
|
||||
*/
|
||||
public static final long APP_RDF_XML = 201;
|
||||
|
||||
/**
|
||||
* Corresponds to number 202 (no standard but defined for very selfish reasons)
|
||||
*/
|
||||
public static final long APP_TURTLE = 202;
|
||||
|
||||
/**
|
||||
* Corresponds to number 203 (no standard but defined for very selfish reasons)
|
||||
*/
|
||||
public static final long APP_N3 = 203;
|
||||
|
||||
/**
|
||||
* Corresponds to number 205 (no standard but defined for very selfish reasons)
|
||||
*/
|
||||
public static final long APP_SHDT = 205;
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Slf4j
|
||||
public final class EmptyOptionValue extends OptionValue<Void> {
|
||||
|
||||
/**
|
||||
* @param optionNumber the option number of the {@link EmptyOptionValue} to be created
|
||||
*
|
||||
* @throws java.lang.IllegalArgumentException if the given option number does not refer to an empty option
|
||||
*/
|
||||
public EmptyOptionValue(int optionNumber) throws IllegalArgumentException {
|
||||
super(optionNumber, new byte[0], false);
|
||||
log.debug("Empty Option (#{}) created.", optionNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>null</code>
|
||||
* @return <code>null</code>
|
||||
*/
|
||||
@Override
|
||||
public Void getDecodedValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns <code>0</code>
|
||||
* @return <code>0</code>
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given {@link Object} equals this {@link EmptyOptionValue} instance. A given {@link Object} equals
|
||||
* this {@link EmptyOptionValue} if and only if the {@link Object} is an instance of {@link EmptyOptionValue}.
|
||||
*
|
||||
* @param object the object to check for equality with this instance of {@link EmptyOptionValue}
|
||||
*
|
||||
* @return <code>true</code> if the given {@link Object} is an instance of {@link EmptyOptionValue} and
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof EmptyOptionValue))
|
||||
return false;
|
||||
|
||||
EmptyOptionValue other = (EmptyOptionValue) object;
|
||||
return Arrays.equals(this.getValue(), other.getValue());
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class OpaqueOptionValue extends OptionValue<byte[]> {
|
||||
|
||||
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
public OpaqueOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
|
||||
super(optionNumber, value, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDecodedValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(getDecodedValue());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof OpaqueOptionValue))
|
||||
return false;
|
||||
|
||||
OpaqueOptionValue other = (OpaqueOptionValue) object;
|
||||
return Arrays.equals(this.getValue(), other.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toHexString(this.value);
|
||||
}
|
||||
|
||||
public static String toHexString(byte[] bytes) {
|
||||
if (bytes.length == 0)
|
||||
return "<empty>";
|
||||
else
|
||||
return "0x" + bytesToHex(bytes);
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
}
|
@ -0,0 +1,449 @@
|
||||
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import com.google.common.collect.HashBasedTable;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import static com.fastbee.coap.model.options.Option.Occurence.*;
|
||||
import static com.fastbee.coap.model.MessageCode.*;
|
||||
|
||||
public abstract class Option {
|
||||
|
||||
public enum Occurence {
|
||||
NONE, ONCE, MULTIPLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to option number -1 = unknown)
|
||||
*/
|
||||
public static final int UNKNOWN = -1;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 1
|
||||
*/
|
||||
public static final int IF_MATCH = 1;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 3
|
||||
*/
|
||||
public static final int URI_HOST = 3;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 4
|
||||
*/
|
||||
public static final int ETAG = 4;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 5
|
||||
*/
|
||||
public static final int IF_NONE_MATCH = 5;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 6
|
||||
*/
|
||||
public static final int OBSERVE = 6;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 7
|
||||
*/
|
||||
public static final int URI_PORT = 7;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 8
|
||||
*/
|
||||
public static final int LOCATION_PATH = 8;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 11
|
||||
*/
|
||||
public static final int URI_PATH = 11;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 12
|
||||
*/
|
||||
public static final int CONTENT_FORMAT = 12;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 14
|
||||
*/
|
||||
public static final int MAX_AGE = 14;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 15
|
||||
*/
|
||||
public static final int URI_QUERY = 15;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 17
|
||||
*/
|
||||
public static final int ACCEPT = 17;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 20
|
||||
*/
|
||||
public static final int LOCATION_QUERY = 20;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 23
|
||||
*/
|
||||
public static final int BLOCK_2 = 23;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 27
|
||||
*/
|
||||
public static final int BLOCK_1 = 27;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 28
|
||||
*/
|
||||
public static final int SIZE_2 = 28;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 35
|
||||
*/
|
||||
public static final int PROXY_URI = 35;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 39
|
||||
*/
|
||||
public static final int PROXY_SCHEME = 39;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 60
|
||||
*/
|
||||
public static final int SIZE_1 = 60;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 124
|
||||
*/
|
||||
public static final int ENDPOINT_ID_1 = 124;
|
||||
|
||||
/**
|
||||
* Corresponds to option number 189
|
||||
*/
|
||||
public static final int ENDPOINT_ID_2 = 189;
|
||||
|
||||
private static HashMap<Integer, String> OPTIONS = new HashMap<>();
|
||||
static {
|
||||
OPTIONS.putAll(ImmutableMap.<Integer, String>builder()
|
||||
.put(IF_MATCH, "IF MATCH (" + IF_MATCH + ")")
|
||||
.put(URI_HOST, "URI HOST (" + URI_HOST + ")")
|
||||
.put(ETAG, "ETAG (" + ETAG + ")")
|
||||
.put(IF_NONE_MATCH, "IF NONE MATCH (" + IF_NONE_MATCH + ")")
|
||||
.put(OBSERVE, "OBSERVE (" + OBSERVE + ")")
|
||||
.put(URI_PORT, "URI PORT (" + URI_PORT + ")")
|
||||
.put(LOCATION_PATH, "LOCATION PATH (" + LOCATION_PATH + ")")
|
||||
.put(URI_PATH, "URI PATH (" + URI_PATH + ")")
|
||||
.put(CONTENT_FORMAT, "CONTENT FORMAT (" + CONTENT_FORMAT + ")")
|
||||
.put(MAX_AGE, "MAX AGE (" + MAX_AGE + ")")
|
||||
.put(URI_QUERY, "URI QUERY (" + URI_QUERY + ")")
|
||||
.put(ACCEPT, "ACCEPT (" + ACCEPT + ")")
|
||||
.put(LOCATION_QUERY, "LOCATION QUERY (" + LOCATION_QUERY + ")")
|
||||
.put(BLOCK_2, "BLOCK 2 (" + BLOCK_2 + ")")
|
||||
.put(BLOCK_1, "BLOCK 1 (" + BLOCK_1 + ")")
|
||||
.put(SIZE_2, "SIZE 2 (" + SIZE_2 + ")")
|
||||
.put(PROXY_URI, "PROXY URI (" + PROXY_URI + ")")
|
||||
.put(PROXY_SCHEME, "PROXY SCHEME (" + PROXY_SCHEME + ")")
|
||||
.put(SIZE_1, "SIZE 1 (" + SIZE_1 + ")")
|
||||
.put(ENDPOINT_ID_1, "ENDPOINT ID 1 (" + ENDPOINT_ID_1 + ")")
|
||||
.put(ENDPOINT_ID_2, "ENDPOINT ID 2 (" + ENDPOINT_ID_2 + ")")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static String asString(int optionNumber) {
|
||||
String result = OPTIONS.get(optionNumber);
|
||||
return result == null ? "UNKOWN (" + optionNumber + ")" : result;
|
||||
}
|
||||
|
||||
private static HashMultimap<Integer, Integer> MUTUAL_EXCLUSIONS = HashMultimap.create();
|
||||
|
||||
static {
|
||||
MUTUAL_EXCLUSIONS.put(URI_HOST, PROXY_URI);
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_HOST);
|
||||
|
||||
MUTUAL_EXCLUSIONS.put(URI_PORT, PROXY_URI);
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_PORT);
|
||||
|
||||
MUTUAL_EXCLUSIONS.put(URI_PATH, PROXY_URI);
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_PATH);
|
||||
|
||||
MUTUAL_EXCLUSIONS.put(URI_QUERY, PROXY_URI);
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_URI, URI_QUERY);
|
||||
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_SCHEME, PROXY_URI);
|
||||
MUTUAL_EXCLUSIONS.put(PROXY_URI, PROXY_SCHEME);
|
||||
}
|
||||
|
||||
public static boolean mutuallyExcludes(int firstOptionNumber, int secondOptionNumber) {
|
||||
return MUTUAL_EXCLUSIONS.get(firstOptionNumber).contains(secondOptionNumber);
|
||||
}
|
||||
|
||||
|
||||
private static final HashBasedTable<Integer, Integer, Option.Occurence> OCCURENCE_CONSTRAINTS
|
||||
= HashBasedTable.create();
|
||||
|
||||
static {
|
||||
// GET Requests
|
||||
OCCURENCE_CONSTRAINTS.row(GET).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(URI_HOST, ONCE)
|
||||
.put(URI_PORT, ONCE)
|
||||
.put(URI_PATH, MULTIPLE)
|
||||
.put(URI_QUERY, MULTIPLE)
|
||||
.put(PROXY_URI, ONCE)
|
||||
.put(PROXY_SCHEME, ONCE)
|
||||
.put(ACCEPT, MULTIPLE)
|
||||
.put(ETAG, MULTIPLE)
|
||||
.put(OBSERVE, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
// POST Requests
|
||||
OCCURENCE_CONSTRAINTS.row(POST).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(URI_HOST, ONCE)
|
||||
.put(URI_PORT, ONCE)
|
||||
.put(URI_PATH, MULTIPLE)
|
||||
.put(URI_QUERY, MULTIPLE)
|
||||
.put(ACCEPT, MULTIPLE)
|
||||
.put(PROXY_URI, ONCE)
|
||||
.put(PROXY_SCHEME, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(SIZE_1, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
// PUT Requests
|
||||
OCCURENCE_CONSTRAINTS.row(PUT).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(URI_HOST, ONCE)
|
||||
.put(URI_PORT, ONCE)
|
||||
.put(URI_PATH, MULTIPLE)
|
||||
.put(URI_QUERY, MULTIPLE)
|
||||
.put(ACCEPT, MULTIPLE)
|
||||
.put(PROXY_URI, ONCE)
|
||||
.put(PROXY_SCHEME, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(IF_MATCH, ONCE)
|
||||
.put(IF_NONE_MATCH, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(SIZE_1, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
// DELETE Requests
|
||||
OCCURENCE_CONSTRAINTS.row(DELETE).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(URI_HOST, ONCE)
|
||||
.put(URI_PORT, ONCE)
|
||||
.put(URI_PATH, MULTIPLE)
|
||||
.put(URI_QUERY, MULTIPLE)
|
||||
.put(PROXY_URI, ONCE)
|
||||
.put(PROXY_SCHEME, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
//Response success (2.x)
|
||||
OCCURENCE_CONSTRAINTS.row(CREATED_201).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(ETAG, ONCE)
|
||||
.put(OBSERVE, ONCE)
|
||||
.put(LOCATION_PATH, MULTIPLE)
|
||||
.put(LOCATION_QUERY, MULTIPLE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(DELETED_202).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(VALID_203).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(OBSERVE, ONCE)
|
||||
.put(ETAG, ONCE)
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(CHANGED_204).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(ETAG, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(CONTENT_205).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(OBSERVE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(ETAG, ONCE)
|
||||
.put(BLOCK_2, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_2, ONCE)
|
||||
.put(ENDPOINT_ID_1, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(CONTINUE_231).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(BLOCK_1, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
// Client ERROR Responses (4.x)
|
||||
OCCURENCE_CONSTRAINTS.row(BAD_REQUEST_400).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(UNAUTHORIZED_401).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(BAD_OPTION_402).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(FORBIDDEN_403).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(NOT_FOUND_404).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(METHOD_NOT_ALLOWED_405).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(NOT_ACCEPTABLE_406).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(REQUEST_ENTITY_INCOMPLETE_408).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(PRECONDITION_FAILED_412).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(REQUEST_ENTITY_TOO_LARGE_413).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(BLOCK_1, ONCE)
|
||||
.put(SIZE_1, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(UNSUPPORTED_CONTENT_FORMAT_415).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
// Server ERROR Responses ( 5.x )
|
||||
OCCURENCE_CONSTRAINTS.row(INTERNAL_SERVER_ERROR_500).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(NOT_IMPLEMENTED_501).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(BAD_GATEWAY_502).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(GATEWAY_TIMEOUT_504).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
|
||||
OCCURENCE_CONSTRAINTS.row(PROXYING_NOT_SUPPORTED_505).putAll(ImmutableMap.<Integer, Occurence>builder()
|
||||
.put(MAX_AGE, ONCE)
|
||||
.put(CONTENT_FORMAT, ONCE)
|
||||
.put(ENDPOINT_ID_2, ONCE)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static Occurence getPermittedOccurrence(int optionNumber, int messageCode) {
|
||||
Occurence result = OCCURENCE_CONSTRAINTS.get(messageCode, optionNumber);
|
||||
return result == null ? NONE : result;
|
||||
}
|
||||
|
||||
public static boolean isCritical(int optionNumber) {
|
||||
return (optionNumber & 1) == 1;
|
||||
}
|
||||
|
||||
public static boolean isSafe(int optionNumber) {
|
||||
return !((optionNumber & 2) == 2);
|
||||
}
|
||||
|
||||
public static boolean isCacheKey(int optionNumber) {
|
||||
return !((optionNumber & 0x1e) == 0x1c);
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static com.fastbee.coap.model.options.OptionValue.Type.*;
|
||||
|
||||
|
||||
public abstract class OptionValue<T>{
|
||||
|
||||
private static final String UNKNOWN_OPTION = "Unknown option no. %d";
|
||||
private static final String VALUE_IS_DEFAULT_VALUE = "Given value is default value for option no. %d.";
|
||||
private static final String OUT_OF_ALLOWED_RANGE = "Given value length (%d) is out of allowed range " +
|
||||
"for option no. %d (min: %d, max; %d).";
|
||||
|
||||
/**
|
||||
* Provides names of available option types (basically for internal use)
|
||||
*/
|
||||
public static enum Type {
|
||||
EMPTY, STRING, UINT, OPAQUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to 60, i.e. 60 seconds
|
||||
*/
|
||||
public static final long MAX_AGE_DEFAULT = 60;
|
||||
|
||||
/**
|
||||
* Corresponds to the maximum value of the max-age option (app. 136 years)
|
||||
*/
|
||||
public static final long MAX_AGE_MAX = 0xFFFFFFFFL;
|
||||
|
||||
/**
|
||||
* Corresponds to the encoded value of {@link #MAX_AGE_DEFAULT}
|
||||
*/
|
||||
public static final byte[] ENCODED_MAX_AGE_DEFAULT =
|
||||
new BigInteger(1, Longs.toByteArray(MAX_AGE_DEFAULT)).toByteArray();
|
||||
|
||||
/**
|
||||
* Corresponds to 5683
|
||||
*/
|
||||
public static final long URI_PORT_DEFAULT = 5683;
|
||||
|
||||
/**
|
||||
* Corresponds to the encoded value of {@link #URI_PORT_DEFAULT}
|
||||
*/
|
||||
public static final byte[] ENCODED_URI_PORT_DEFAULT =
|
||||
new BigInteger(1, Longs.toByteArray(URI_PORT_DEFAULT)).toByteArray();
|
||||
|
||||
|
||||
private static class Characteristics {
|
||||
private final Type type;
|
||||
private final int minLength;
|
||||
private final int maxLength;
|
||||
|
||||
private Characteristics(Type type, int minLength, int maxLength) {
|
||||
this.type = type;
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getMinLength() {
|
||||
return minLength;
|
||||
}
|
||||
|
||||
public int getMaxLength() {
|
||||
return maxLength;
|
||||
}
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, Characteristics> CHARACTERISTICS = new HashMap<>();
|
||||
static {
|
||||
CHARACTERISTICS.put( Option.IF_MATCH, new Characteristics( OPAQUE, 0, 8 ));
|
||||
CHARACTERISTICS.put( Option.URI_HOST, new Characteristics( STRING, 1, 255 ));
|
||||
CHARACTERISTICS.put( Option.ETAG, new Characteristics( OPAQUE, 1, 8 ));
|
||||
CHARACTERISTICS.put( Option.IF_NONE_MATCH, new Characteristics( EMPTY, 0, 0 ));
|
||||
CHARACTERISTICS.put( Option.URI_PORT, new Characteristics( UINT, 0, 2 ));
|
||||
CHARACTERISTICS.put( Option.LOCATION_PATH, new Characteristics( STRING, 0, 255 ));
|
||||
CHARACTERISTICS.put( Option.OBSERVE, new Characteristics( UINT, 0, 3 ));
|
||||
CHARACTERISTICS.put( Option.URI_PATH, new Characteristics( STRING, 0, 255 ));
|
||||
CHARACTERISTICS.put( Option.CONTENT_FORMAT, new Characteristics( UINT, 0, 2 ));
|
||||
CHARACTERISTICS.put( Option.MAX_AGE, new Characteristics( UINT, 0, 4 ));
|
||||
CHARACTERISTICS.put( Option.URI_QUERY, new Characteristics( STRING, 0, 255 ));
|
||||
CHARACTERISTICS.put( Option.ACCEPT, new Characteristics( UINT, 0, 2 ));
|
||||
CHARACTERISTICS.put( Option.LOCATION_QUERY, new Characteristics( STRING, 0, 255 ));
|
||||
CHARACTERISTICS.put( Option.BLOCK_2, new Characteristics( UINT, 0, 3 ));
|
||||
CHARACTERISTICS.put( Option.BLOCK_1, new Characteristics( UINT, 0, 3 ));
|
||||
CHARACTERISTICS.put( Option.SIZE_2, new Characteristics( UINT, 0, 4 ));
|
||||
CHARACTERISTICS.put( Option.PROXY_URI, new Characteristics( STRING, 1, 1034 ));
|
||||
CHARACTERISTICS.put( Option.PROXY_SCHEME, new Characteristics( STRING, 1, 255 ));
|
||||
CHARACTERISTICS.put( Option.SIZE_1, new Characteristics( UINT, 0, 4 ));
|
||||
CHARACTERISTICS.put( Option.ENDPOINT_ID_1, new Characteristics( OPAQUE, 0, 8 ));
|
||||
CHARACTERISTICS.put( Option.ENDPOINT_ID_2, new Characteristics( OPAQUE, 0, 8 ));
|
||||
}
|
||||
|
||||
public static Type getType(int optionNumber) throws IllegalArgumentException {
|
||||
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
|
||||
if (characteristics == null) {
|
||||
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
|
||||
} else {
|
||||
return characteristics.getType();
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMinLength(int optionNumber) throws IllegalArgumentException {
|
||||
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
|
||||
if (characteristics == null) {
|
||||
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
|
||||
} else {
|
||||
return characteristics.getMinLength();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int getMaxLength(int optionNumber) throws IllegalArgumentException {
|
||||
Characteristics characteristics = CHARACTERISTICS.get(optionNumber);
|
||||
if (characteristics == null) {
|
||||
throw new IllegalArgumentException(String.format(UNKNOWN_OPTION, optionNumber));
|
||||
} else {
|
||||
return characteristics.getMaxLength();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDefaultValue(int optionNumber, byte[] value) {
|
||||
|
||||
if (optionNumber == Option.URI_PORT && Arrays.equals(value, ENCODED_URI_PORT_DEFAULT)) {
|
||||
return true;
|
||||
} else if (optionNumber == Option.MAX_AGE && Arrays.equals(value, ENCODED_MAX_AGE_DEFAULT)) {
|
||||
return true;
|
||||
} else if (optionNumber == Option.URI_HOST) {
|
||||
String hostName = new String(value, CoapMessage.CHARSET);
|
||||
if (hostName.startsWith("[") && hostName.endsWith("]")) {
|
||||
hostName = hostName.substring(1, hostName.length() - 1);
|
||||
}
|
||||
|
||||
if (InetAddresses.isInetAddress(hostName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected byte[] value;
|
||||
|
||||
protected OptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
|
||||
|
||||
if (!allowDefault && OptionValue.isDefaultValue(optionNumber, value)) {
|
||||
throw new IllegalArgumentException(String.format(VALUE_IS_DEFAULT_VALUE, optionNumber));
|
||||
}
|
||||
|
||||
if (getMinLength(optionNumber) > value.length || getMaxLength(optionNumber) < value.length) {
|
||||
throw new IllegalArgumentException(String.format(OUT_OF_ALLOWED_RANGE, value.length, optionNumber,
|
||||
getMinLength(optionNumber), getMaxLength(optionNumber)));
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public abstract T getDecodedValue();
|
||||
|
||||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
@Override
|
||||
public abstract boolean equals(Object object);
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + this.getDecodedValue();
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import com.fastbee.coap.model.CoapMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
@Slf4j
|
||||
public class StringOptionValue extends OptionValue<String> {
|
||||
|
||||
public StringOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
|
||||
this(optionNumber, value, false);
|
||||
}
|
||||
|
||||
public StringOptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
|
||||
super(optionNumber, value, allowDefault);
|
||||
log.debug("String Option (#{}) created with value: '{}'.", optionNumber, this.getDecodedValue());
|
||||
}
|
||||
|
||||
public StringOptionValue(int optionNumber, String value) throws IllegalArgumentException{
|
||||
|
||||
this(optionNumber, optionNumber == Option.URI_HOST ?
|
||||
convertToByteArrayWithoutPercentEncoding(value.toLowerCase(Locale.ENGLISH)) :
|
||||
((optionNumber == Option.URI_PATH || optionNumber == Option.URI_QUERY) ?
|
||||
convertToByteArrayWithoutPercentEncoding(value) :
|
||||
value.getBytes(CoapMessage.CHARSET)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecodedValue() {
|
||||
return new String(value, CoapMessage.CHARSET);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getDecodedValue().hashCode();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof StringOptionValue))
|
||||
return false;
|
||||
|
||||
StringOptionValue other = (StringOptionValue) object;
|
||||
return Arrays.equals(this.getValue(), other.getValue());
|
||||
}
|
||||
|
||||
public static byte[] convertToByteArrayWithoutPercentEncoding(String s) throws IllegalArgumentException{
|
||||
log.debug("With percent encoding: {}", s);
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(s.getBytes(CoapMessage.CHARSET));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int i;
|
||||
do {
|
||||
i = in.read();
|
||||
//-1 indicates end of stream
|
||||
if (i == -1) {
|
||||
break;
|
||||
}
|
||||
//0x25 = '%'
|
||||
if (i == 0x25) {
|
||||
//Character.digit returns the integer value encoded as in.read(). Since we know that percent encoding
|
||||
//uses bytes from 0x0 to 0xF (i.e. 0 to 15) the radix must be 16.
|
||||
int d1 = Character.digit(in.read(), 16);
|
||||
int d2 = Character.digit(in.read(), 16);
|
||||
|
||||
if (d1 == -1 || d2 == -1) {
|
||||
//Unexpected end of stream (at least one byte missing after '%')
|
||||
throw new IllegalArgumentException("Invalid percent encoding in: " + s);
|
||||
}
|
||||
|
||||
//Write decoded value to output stream (e.g. sequence [0x02, 0x00] results into byte 0x20
|
||||
out.write((d1 << 4) | d2);
|
||||
} else {
|
||||
out.write(i);
|
||||
}
|
||||
} while(true);
|
||||
|
||||
byte[] result = out.toByteArray();
|
||||
log.debug("Without percent encoding: {}", new String(result, CoapMessage.CHARSET));
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
|
||||
package com.fastbee.coap.model.options;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Slf4j
|
||||
public class UintOptionValue extends OptionValue<Long> {
|
||||
|
||||
/**
|
||||
* Corresponds to a value of <code>-1</code> to indicate that there is no value for that option set.
|
||||
*/
|
||||
public static final long UNDEFINED = -1;
|
||||
|
||||
public UintOptionValue(int optionNumber, byte[] value) throws IllegalArgumentException {
|
||||
this(optionNumber, shortenValue(value), false);
|
||||
}
|
||||
|
||||
public UintOptionValue(int optionNumber, byte[] value, boolean allowDefault) throws IllegalArgumentException {
|
||||
super(optionNumber, shortenValue(value), allowDefault);
|
||||
log.debug("Uint Option (#{}) created with value: {}", optionNumber, this.getDecodedValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getDecodedValue() {
|
||||
return new BigInteger(1, value).longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getDecodedValue().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof UintOptionValue))
|
||||
return false;
|
||||
|
||||
UintOptionValue other = (UintOptionValue) object;
|
||||
return Arrays.equals(this.getValue(), other.getValue());
|
||||
}
|
||||
|
||||
public static byte[] shortenValue(byte[] value) {
|
||||
int index = 0;
|
||||
while(index < value.length - 1 && value[index] == 0)
|
||||
index++;
|
||||
return Arrays.copyOfRange(value, index, value.length);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.fastbee.coap.server;
|
||||
|
||||
import com.fastbee.coap.codec.CoapMessageDecoder;
|
||||
import com.fastbee.coap.codec.CoapMessageEncoder;
|
||||
import com.fastbee.coap.handler.ReqDispatcher;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
|
||||
|
||||
public class CoapServerChannelInitializer extends ChannelInitializer<DatagramChannel> {
|
||||
|
||||
private final ResourceRegistry resourceRegistry;
|
||||
|
||||
public CoapServerChannelInitializer(ResourceRegistry resourceRegistry) {
|
||||
super();
|
||||
this.resourceRegistry = resourceRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(DatagramChannel datagramChannel) throws Exception {
|
||||
ChannelPipeline p = datagramChannel.pipeline();
|
||||
p.addLast(new CoapMessageEncoder())
|
||||
.addLast(new CoapMessageDecoder())
|
||||
.addLast(new ReqDispatcher(this.resourceRegistry));
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package com.fastbee.coap.server;
|
||||
|
||||
import com.fastbee.coap.handler.ResourceHandler;
|
||||
import com.fastbee.coap.model.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.fastbee.coap.model.MessageCode.BAD_REQUEST_400;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ResourceRegistry{
|
||||
private final Map<String, ResourceHandler> handlers = new HashMap<String, ResourceHandler>();
|
||||
|
||||
public void register(ResourceHandler handler) {
|
||||
handlers.put(handler.getPath(), handler);
|
||||
}
|
||||
|
||||
public CoapMessage respond(CoapRequest request) {
|
||||
// find the URI
|
||||
String url = request.getUriPath();
|
||||
log.debug("requested URL : {}", url);
|
||||
|
||||
if (url.length() < 1) {
|
||||
// 4.00 !
|
||||
return CoapResponse.createErrorResponse(request.getMessageType(), BAD_REQUEST_400,"no URL !");
|
||||
}
|
||||
if (".well-known/core".equalsIgnoreCase(url)) {
|
||||
// discovery !
|
||||
CoapResponse coapResponse = CoapResponse.createErrorResponse(MessageType.ACK, MessageCode.CONTENT_205,"");
|
||||
coapResponse.setContent(getDiscovery());
|
||||
coapResponse.setSender(request.getSender());
|
||||
coapResponse.setMessageID(request.getMessageID());
|
||||
coapResponse.setToken(request.getToken());
|
||||
return coapResponse;
|
||||
} else {
|
||||
ResourceHandler handler = handlers.get(url);
|
||||
if (handler == null) {
|
||||
// 4.04 !
|
||||
CoapResponse coapResponse = CoapResponse.createErrorResponse(MessageType.ACK, MessageCode.NOT_FOUND_404,"");
|
||||
coapResponse.setContent("not found !".getBytes());
|
||||
coapResponse.setSender(request.getSender());
|
||||
return coapResponse;
|
||||
} else {
|
||||
return handler.handle(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getDiscovery() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, ResourceHandler> e : handlers.entrySet()) {
|
||||
// ex :</link1>;if="If1";rt="Type1 Type2";title="Link test resource",
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
b.append(",");
|
||||
}
|
||||
ResourceHandler h = e.getValue();
|
||||
b.append("</").append(h.getPath()).append(">");
|
||||
if (h.getInterface() != null) {
|
||||
b.append(";if=\"").append(h.getInterface()).append("\"");
|
||||
}
|
||||
if (h.getResourceType() != null) {
|
||||
b.append(";rt=\"").append(h.getResourceType()).append("\"");
|
||||
}
|
||||
if (h.getTitle() != null) {
|
||||
b.append(";title=\"").append(h.getTitle()).append("\"");
|
||||
}
|
||||
}
|
||||
return b.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user