第一次提交

This commit is contained in:
wyw
2024-08-08 00:31:26 +08:00
commit c202e2b63d
1819 changed files with 221890 additions and 0 deletions

View 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>

View File

@ -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));
}
}

View File

@ -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 消息分发处理

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<>();
}
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 + ")";
}
}

View File

@ -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])));
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}