commit 6ae2ece6d66bb69c421c34280b2cf1c1da4defc6 Author: yaohunya <1763917516@qq.com> Date: Fri Jul 18 22:50:07 2025 +0800 1.2.0 diff --git a/lib/BungeeCord.jar b/lib/BungeeCord.jar new file mode 100644 index 0000000..2ea6995 Binary files /dev/null and b/lib/BungeeCord.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c0afcdf --- /dev/null +++ b/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.yaohun.guaji.AuGuaJi + AuMcBot + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + public + https://repo.aurora-pixels.com/repository/public/ + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/yaohun/mcbot/McBot.java b/src/main/java/com/yaohun/mcbot/McBot.java new file mode 100644 index 0000000..4a6e6d8 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/McBot.java @@ -0,0 +1,66 @@ +package com.yaohun.mcbot; + +import com.yaohun.mcbot.client.QQWebSocketClient; +import com.yaohun.mcbot.commands.BindAdminCmd; +import com.yaohun.mcbot.commands.BindTencentCmd; +import com.yaohun.mcbot.config.Config; +import com.yaohun.mcbot.data.sql.SQLIO; +import com.yaohun.mcbot.listsener.FriendListener; +import com.yaohun.mcbot.listsener.GroupListener; +import com.yaohun.mcbot.manage.CacheManager; +import com.yaohun.mcbot.manage.WssReconnectManager; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.plugin.Plugin; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class McBot extends Plugin { + + private static McBot instance; + + private static WssReconnectManager wssManager; + + @Override + public void onEnable() { + instance = this; + getLogger().info("[McBot] Start Plugins ..."); + Config.reloadConfig(this); + SQLIO.init(); // 激活MySQL - 数据库的连接 + + String serverUri = "ws://127.0.0.1:6050"; + WssReconnectManager.initialize(this, serverUri); + + QQWebSocketClient client = new QQWebSocketClient(serverUri); + WssReconnectManager.setClient(client); + client.connect(); + + getProxy().getPluginManager().registerCommand(this, new BindTencentCmd()); + getProxy().getPluginManager().registerCommand(this, new BindAdminCmd()); + + getProxy().getPluginManager().registerListener(this, new GroupListener()); + getProxy().getPluginManager().registerListener(this, new FriendListener()); + + ProxyServer.getInstance().getScheduler().schedule(this, CacheManager::checkingCleanUpData, 1L, 5L, TimeUnit.MINUTES); + } + + @Override + public void onDisable() { + SQLIO.closeConnection(); // 关闭MySQL - 数据库的连接 + getLogger().info("[WebSocket] 插件卸载,准备关闭 WebSocket 通道..."); + QQWebSocketClient client = WssReconnectManager.getClient(); + if (client != null && client.isOpen()) { + try { + client.close(); // 发送关闭帧 + 关闭资源 + getLogger().info("[WebSocket] WebSocket 通道已成功关闭。"); + } catch (Exception e) { + getLogger().warning("[WebSocket] 关闭通道时出现异常: " + e.getMessage()); + } + } + } + + + public static McBot inst() { + return instance; + } +} diff --git a/src/main/java/com/yaohun/mcbot/MessageForwarder.java b/src/main/java/com/yaohun/mcbot/MessageForwarder.java new file mode 100644 index 0000000..a0e7712 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/MessageForwarder.java @@ -0,0 +1,92 @@ +package com.yaohun.mcbot; + + +import com.yaohun.mcbot.data.BotFriend; +import com.yaohun.mcbot.data.BotGroup; +import com.yaohun.mcbot.event.BotFriendMessageEvent; +import com.yaohun.mcbot.event.BotGroupMessageEvent; +import net.md_5.bungee.api.ProxyServer; +import org.json.JSONArray; +import org.json.JSONObject; + +public class MessageForwarder { + + public static void friendsAnalyze(String json){ + try { + JSONObject obj = new JSONObject(json); + // 检测类型是否是 “好友消息” 若不是则拦截返回 + if (!"private".equals(obj.optString("message_type"))) return; + // 获取机器人的QQ号 + long botId = obj.optLong("self_id"); + // 获取聊天信息内容 若没有消息则返回 + JSONArray messageArray = obj.optJSONArray("message"); + if (messageArray == null) return; + // 将聊天信息内容 builder + StringBuilder finalMessage = new StringBuilder(); + for (int i = 0; i < messageArray.length(); i++) { + JSONObject segment = messageArray.getJSONObject(i); + String type = segment.optString("type"); + if ("text".equals(type)) { + String text = segment.getJSONObject("data").optString("text"); + finalMessage.append(text); + } + // 可以在这里加对 image、face 等其他类型的支持 + } + String message = finalMessage.toString(); + if(message.isEmpty()) return; + // 解析发送信息的人 + JSONObject sender = obj.optJSONObject("sender"); + if (sender == null) return; + // 获取信息 + long senderId = sender.optLong("user_id"); // QQ号 + String nickname = sender.optString("nickname"); // QQ昵称 + BotFriendMessageEvent event = new BotFriendMessageEvent(botId,senderId,nickname,message,new BotFriend(senderId)); + ProxyServer.getInstance().getPluginManager().callEvent(event); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void groupAnalyze(String json) { + try { + JSONObject obj = new JSONObject(json); + // 检测类型是否是 “群消息” 若不是则拦截返回 + if (!"group".equals(obj.optString("message_type"))) return; + // 获取机器人的QQ号 + long botId = obj.optLong("self_id"); + long groupId = obj.optLong("group_id"); + // 获取聊天信息内容 若没有消息则返回 + JSONArray messageArray = obj.optJSONArray("message"); + if (messageArray == null) return; + // 将聊天信息内容 builder + StringBuilder finalMessage = new StringBuilder(); + for (int i = 0; i < messageArray.length(); i++) { + JSONObject segment = messageArray.getJSONObject(i); + String type = segment.optString("type"); + if ("text".equals(type)) { + String text = segment.getJSONObject("data").optString("text"); + finalMessage.append(text); + } + // 可以在这里加对 image、face 等其他类型的支持 + } + String message = finalMessage.toString(); + if(message.isEmpty()) return; + // 解析发送信息的人 + JSONObject sender = obj.optJSONObject("sender"); + if (sender == null) return; + // 获取信息 + long senderId = sender.optLong("user_id"); // QQ号 + String nickname = sender.optString("nickname"); // QQ昵称 + String card = sender.optString("card"); // 群名片 + String role = sender.optString("role"); // 群中职务 + // 优先显示群名片(如果存在),否则用昵称 + String displayName = (card != null && !card.isEmpty()) ? card : nickname; + + BotGroupMessageEvent event = new BotGroupMessageEvent(botId,groupId,senderId,displayName,message,new BotGroup(groupId)); + ProxyServer.getInstance().getPluginManager().callEvent(event); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/yaohun/mcbot/api/McBotAPI.java b/src/main/java/com/yaohun/mcbot/api/McBotAPI.java new file mode 100644 index 0000000..42b75f9 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/api/McBotAPI.java @@ -0,0 +1,16 @@ +package com.yaohun.mcbot.api; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.data.BotFriend; +import com.yaohun.mcbot.data.BotGroup; + +public class McBotAPI { + + public static BotFriend getFriend(long senderID){ + return new BotFriend(senderID); + } + + public static BotGroup getGroup(long groupID){ + return new BotGroup(groupID); + } +} diff --git a/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java b/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java new file mode 100644 index 0000000..3a1a25d --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/client/QQWebSocketClient.java @@ -0,0 +1,53 @@ +package com.yaohun.mcbot.client; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.MessageForwarder; +import com.yaohun.mcbot.manage.WssReconnectManager; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.json.JSONObject; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class QQWebSocketClient extends WebSocketClient { + + public QQWebSocketClient( String serverUri) { + super(URI.create(serverUri)); + } + + @Override + public void onOpen(ServerHandshake handshake) { + McBot.inst().getLogger().info("[WebSocket-Link] connectionSuccessfully!"); + } + + @Override + public void onMessage(String message) { + JSONObject json = new JSONObject(message); + if ("group".contains(json.optString("message_type"))){ + MessageForwarder.groupAnalyze(message); + // System.out.println("[WebSocket-Group] "+json.getString("raw_message")); + } else if ("private".contains(json.optString("message_type"))){ + MessageForwarder.friendsAnalyze(message); + // System.out.println("[WebSocket-Friends] "+json.getString("raw_message")); + } else { + System.out.println("[WebSocket-MSG] "+json); + } + } + + + @Override + public void onClose(int code, String reason, boolean remote) { + WssReconnectManager.tryReconnect(); + } + + @Override + public void onError(Exception ex) { + WssReconnectManager.tryReconnect(); + } + +} diff --git a/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java b/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java new file mode 100644 index 0000000..dc3031b --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/commands/BindAdminCmd.java @@ -0,0 +1,75 @@ +package com.yaohun.mcbot.commands; + +import com.yaohun.mcbot.config.Config; +import com.yaohun.mcbot.data.sql.SQLIO; +import com.yaohun.mcbot.manage.CacheManager; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; + +import java.util.List; + +public class BindAdminCmd extends Command { + + + private static String prefix; + + public BindAdminCmd() { + super("bindadmin"); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if(!sender.hasPermission("bungeecord.*")){return;} + if(args.length == 0){ + sender.sendMessage("§e------- ======= §6妖魂的MC机器人 §e======= -------"); + sender.sendMessage("§2/bindadmin clear §f- §2清理所有缓存"); + sender.sendMessage("§2/bindadmin change §e[玩家名] §b<企鹅号> §f- §2修改绑定"); + sender.sendMessage("§2/bindadmin group §e[群号] §f- §2新增/删除检测群"); + sender.sendMessage("§e------- ======= §6妖魂的MC机器人 §e======= -------"); + return; + } + if(args.length == 1 && args[0].equalsIgnoreCase("clear")){ + CacheManager.codeDataMap.clear(); + CacheManager.randomCodeMap.clear(); + sender.sendMessage("[Bot管理] 所有缓存数据已被清理"); + return; + } + if(args[0].equalsIgnoreCase("change")){ + if(args.length == 1){ + sender.sendMessage("[Bot管理] 请填入正确的玩家名."); + return; + } + if(args.length == 2){ + sender.sendMessage("[Bot管理] 请填入正确的企鹅号."); + return; + } + String playerName = args[1]; + String senderID = args[2]; + SQLIO.saveGroup(playerName, senderID); + sender.sendMessage("[Bot管理] 已更改 "+playerName+" 的绑定为 <"+senderID+">"); + return; + } + if(args[0].equalsIgnoreCase("group")){ + if(args.length == 1){ + sender.sendMessage("[Bot管理] 请填写群号."); + return; + } + long groupID = Long.parseLong(args[1]); + List groupAccountList = Config.BOT_GroupAccounts; + if(groupAccountList.contains(groupID)){ + Config.BOT_GroupAccounts.remove(groupID); + sender.sendMessage("[Bot管理] 已移除检测群: "+groupID); + } else { + Config.BOT_GroupAccounts.add(groupID); + sender.sendMessage("[Bot管理] 已添加检测群: "+groupID); + } + Config.saveBOT_GroupAccounts(); + return; + } + } +} diff --git a/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java b/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java new file mode 100644 index 0000000..b819800 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/commands/BindTencentCmd.java @@ -0,0 +1,91 @@ +package com.yaohun.mcbot.commands; + +import com.yaohun.mcbot.config.Config; +import com.yaohun.mcbot.data.sql.SQLIO; +import com.yaohun.mcbot.manage.CacheManager; +import gnu.trove.stack.TLongStack; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; + +public class BindTencentCmd extends Command { + + private static String prefix; + + public BindTencentCmd() { + super("bind"); + prefix = Config.getMessage("prefix"); + } + + @Override + public void execute(CommandSender sender, String[] args) { + // 首先判断输入此命令的是否是玩家 + if(sender instanceof ProxiedPlayer) { + if (args.length == 2) { + String tencentQQ = args[0].replaceAll("\\[|\\]", ""); + String tencentQQRepeat = args[1].replaceAll("\\[|\\]", ""); + ProxiedPlayer player = (ProxiedPlayer) sender; + if (!tencentQQ.equalsIgnoreCase(tencentQQRepeat)) { + String msg = Config.getMessage("differentTwice"); + player.sendMessage(prefix+msg); + return; + } + String userName = player.getName(); + String cacheCode = CacheManager.getRandomCodeData(userName); + if(cacheCode != null) { + sender.sendMessage(" "); + sender.sendMessage(" "); + if(!Config.Group_Info.isEmpty()) { + sender.sendMessage(Config.Group_Info); + } + SendCopyMessage(player,cacheCode); + sender.sendMessage(" "); + return; + } + // 判断玩家是否已绑定QQ号 若已绑定则拦截数据 + String userID = SQLIO.getUserID(userName); + if (userID != null) { + String message = Config.getMessage("alreadyBindUserID"); + player.sendMessage(prefix + message.replace("%qq%", userID)); + return; + } + // 判断QQ是否已绑定过账号 若已绑定则连接数据 + String user = SQLIO.getUserName(tencentQQ); + if (user != null) { + String message = Config.getMessage("alreadyBindUserName"); + player.sendMessage(prefix + message.replace("%user%", user)); + return; + } + // 获取绑定验证码发送给玩家 + String randomCode = Config.getRandomString(); + CacheManager.setRandomCodeData(userName, randomCode); // 将验证码存入缓存 + CacheManager.setCodeDataMap(randomCode,new CacheManager.CodeData(userName,randomCode,tencentQQ)); + sender.sendMessage(" "); + sender.sendMessage(" "); + if(!Config.Group_Info.isEmpty()) { + sender.sendMessage(Config.Group_Info); + } + SendCopyMessage(player, randomCode); + sender.sendMessage(" "); + } else { + String msg = Config.getMessage("correctOperate"); + sender.sendMessage(prefix+msg); + } + } else { + sender.sendMessage("§c此命令仅限以玩家身份执行."); + } + } + + public void SendCopyMessage(ProxiedPlayer player,String bind_code) { + String message = prefix+Config.getMessage("verificationCode").replace("%code%",bind_code); + TextComponent component = new TextComponent(message); + System.out.print("玩家: "+player.getName()+" randomCode: "+bind_code); + component.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/绑定 "+bind_code)); + component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§e§l/绑定 "+bind_code).create())); + player.sendMessage(component); + } +} diff --git a/src/main/java/com/yaohun/mcbot/config/Config.java b/src/main/java/com/yaohun/mcbot/config/Config.java new file mode 100644 index 0000000..2ede2fd --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/config/Config.java @@ -0,0 +1,126 @@ +package com.yaohun.mcbot.config; + +import com.google.common.io.ByteStreams; +import com.yaohun.mcbot.McBot; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Random; + +public class Config { + + private static Configuration config; + public static String SQL_Host; + public static String SQL_Port; + public static String SQL_Database; + public static String SQL_Users; + public static String SQL_Password; + public static List BOT_GroupAccounts = new ArrayList<>(); + public static String Group_Info = ""; + private static HashMap messageMap = new HashMap<>(); + + public static void reloadConfig(McBot plugin) { + if(!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdir(); + } + File file = new File(plugin.getDataFolder(), "config.yml"); + if(!file.exists()) { + try { + file.createNewFile(); + try (InputStream is = plugin.getResourceAsStream("config.yml"); + OutputStream os = Files.newOutputStream(file.toPath())) { + ByteStreams.copy(is, os); + } + } catch (IOException e) { + throw new RuntimeException("Unable to create configuration file", e); + } + } + try { + ConfigurationProvider cfg = ConfigurationProvider.getProvider(YamlConfiguration.class); + config = cfg.load(file); + loadMySQLData(); + loadConfigData(); + loadMessageData(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void saveBOT_GroupAccounts() { + List groupAccountList = new ArrayList<>(); + for (Long l : BOT_GroupAccounts) { + groupAccountList.add(l.toString()); + } + File file = new File(McBot.inst().getDataFolder(), "config.yml"); + // 转换 List 为 List,避免 YAML 不兼容 Long 处理 + config.set("Group-Numbers", groupAccountList); + try { + ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, file); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void loadMySQLData(){ + SQL_Host = config.getString("MYSQL.Host"); + SQL_Port = config.getString("MYSQL.Port"); + SQL_Database = config.getString("MYSQL.Database"); + SQL_Users = config.getString("MYSQL.Users"); + SQL_Password = config.getString("MYSQL.Password"); + } + + private static void loadConfigData(){ + List stringList = config.getStringList("Group-Numbers"); + System.out.println("[AuBot] 监听QQ群: "); + for (String value : stringList) { + BOT_GroupAccounts.add(Long.parseLong(value)); + System.out.println("- "+value); + } + Group_Info = config.getString("Group_Info","").replace("&","§"); + } + + private static void loadMessageData(){ + if(config.getString("Message") == null) return; + for (String key : config.getSection("Message").getKeys()){ + String value = config.getString("Message." + key); + messageMap.put(key, value.replace("&","§")); + } + System.out.println("[AuBot] 语言参数 "+messageMap.size()+"个"); + } + + public static String getMessage(String key){ + if(messageMap.containsKey(key)){ + return messageMap.get(key); + } + return "缺少节点."+key; + } + + public static String getRandomString() { + String str = "CEFGHIJKLMNPQTXY34679"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 5; i++) { + int number = random.nextInt(21); + sb.append(str.charAt(number)); + } + return sb.toString(); + } + + public static String extractBindCode(String message) { + return message + .replace("/绑定 ", "") + .replace(" ", "") + .toUpperCase() + .replaceAll("[^A-Z0-9]", ""); + } + +} diff --git a/src/main/java/com/yaohun/mcbot/data/BotFriend.java b/src/main/java/com/yaohun/mcbot/data/BotFriend.java new file mode 100644 index 0000000..2c472a0 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/data/BotFriend.java @@ -0,0 +1,27 @@ +package com.yaohun.mcbot.data; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.manage.WssReconnectManager; + +public class BotFriend { + + private long userId; + + public BotFriend(long userId) { + this.userId = userId; + } + + public void sendMessage(String message){ + String escapedMessage = message + .replace("\\", "\\\\") // 转义反斜线 + .replace("\"", "\\\"") // 转义引号 + .replace("\n", "\\n") // 转义换行符 + .replace("\r", "\\r"); // 转义回车符(保险起见) + + String payload = String.format( + "{\"action\":\"send_private_msg\",\"params\":{\"user_id\":%d,\"message\":\"%s\"}}", + userId, escapedMessage + ); + WssReconnectManager.getClient().send(payload); + } +} diff --git a/src/main/java/com/yaohun/mcbot/data/BotGroup.java b/src/main/java/com/yaohun/mcbot/data/BotGroup.java new file mode 100644 index 0000000..3ae0945 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/data/BotGroup.java @@ -0,0 +1,27 @@ +package com.yaohun.mcbot.data; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.manage.WssReconnectManager; + +public class BotGroup { + + private long groupId; + + public BotGroup(long groupId) { + this.groupId = groupId; + } + + public void sendMessage(String message){ + String escapedMessage = message + .replace("\\", "\\\\") // 转义反斜线 + .replace("\"", "\\\"") // 转义引号 + .replace("\n", "\\n") // 转义换行符 + .replace("\r", "\\r"); // 转义回车符(保险起见) + + String payload = String.format( + "{\"action\":\"send_group_msg\",\"params\":{\"group_id\":%d,\"message\":\"%s\"}}", + groupId, escapedMessage + ); + WssReconnectManager.getClient().send(payload); + } +} diff --git a/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java b/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java new file mode 100644 index 0000000..4a90cb7 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/data/sql/SQLIO.java @@ -0,0 +1,179 @@ +package com.yaohun.mcbot.data.sql; + +import com.yaohun.mcbot.config.Config; + +import java.sql.*; + +public class SQLIO { + + private static String host; + private static int port; + private static String database; + private static String username; + private static String password; + private static String tableName; + + private static Connection connection; + + public static void init(){ + host = Config.SQL_Host; + port = Integer.parseInt(Config.SQL_Port); + database = Config.SQL_Database; + username = Config.SQL_Users; + password = Config.SQL_Password; + tableName = "binddata"; + try { + Class.forName("com.mysql.jdbc.Driver"); + getConnection(); + initTable(); + } catch (Exception e){ + e.printStackTrace(); + } + } + + private static void initTable() { + try { + Statement stat = getConnection().createStatement(); + stat.execute( + "CREATE TABLE IF NOT EXISTS "+tableName+" (" + + " username VARCHAR(64) NOT NULL," + + " userid VARCHAR(64) NOT NULL," + + " PRIMARY KEY (username)" + + ")" + ); + stat.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static boolean hasUserNameExit(String userName) { + try { + try (PreparedStatement ps = getConnection().prepareStatement("SELECT `userid` FROM "+tableName+" WHERE `username` = ? LIMIT 1")) { + ps.setString(1, userName); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static boolean hasUserIDExit(String userID) { + try { + String sql = "SELECT `username` FROM " + tableName + " WHERE `userid` = ? LIMIT 1"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, userID); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); // 查询到了,说明存在 + } + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static void saveGroup(String userName, String userID) { + if (!SQLIO.hasUserNameExit(userName)) { + insertGroup(userName, userID); + } else { + updateGroup(userName, userID); + } + } + + private static void insertGroup(String userName, String userID) { + try { + String sql = "INSERT INTO "+tableName+"(username, userid) VALUES(?,?);"; + try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) { + ps.setString(1, userName); + ps.setString(2 , userID); + ps.executeUpdate(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void updateGroup(String userName, String userID) { + try { + String sql = "UPDATE `"+tableName+"` SET `userid` = ? WHERE `username` = ? LIMIT 1"; + try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) { + ps.setString(1, userID); + ps.setString(2, userName); + ps.executeUpdate(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String getUserID(String userName) { + try { + String sql = "SELECT `userid` FROM `"+tableName+"` WHERE `username` = ? LIMIT 1"; + try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) { + ps.setString(1, userName); + try (ResultSet resultSet = ps.executeQuery()) { + if (resultSet.next()) { + return resultSet.getString(1); + } + } + } + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static String getUserName(String userID) { + try { + String sql = "SELECT `username` FROM `" + tableName + "` WHERE `userid` = ? LIMIT 1"; + try (PreparedStatement ps = SQLIO.getConnection().prepareStatement(sql)) { + ps.setString(1, userID); + try (ResultSet resultSet = ps.executeQuery()) { + if (resultSet.next()) { + return resultSet.getString(1); // 返回 username + } + } + } + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static Connection getConnection() { + try { + if (connection == null) { + String url = "jdbc:mysql://" + host + ":" + port + "/" + database + + "?autoReconnect=true" + + "&failOverReadOnly=false" + + "&allowMultiQueries=true" + + "&useSSL=false" + + "&useUnicode=true" + + "&characterEncoding=utf8"; + connection = DriverManager.getConnection(url, username, password); + System.out.println("[AuBot] 数据库: "+host+":"+port+"/"+database+" (已正常连接)"); + return connection; + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("[AuBot] 数据库: "+host+":"+port+"/"+database+" (连接失败)"); + } + return connection; + } + + public static void closeConnection() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (Exception e){ + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java b/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java new file mode 100644 index 0000000..42d85eb --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/event/BotFriendMessageEvent.java @@ -0,0 +1,46 @@ +package com.yaohun.mcbot.event; + +import com.yaohun.mcbot.data.BotFriend; +import com.yaohun.mcbot.data.BotGroup; +import net.md_5.bungee.api.plugin.Event; + +public class BotFriendMessageEvent extends Event { + + private long botId; + + private long senderId; + + private String senderName; + + private String message; + + private BotFriend botFriend; + + public BotFriendMessageEvent(long botId, long senderId, String senderName, String message, BotFriend botFriend) { + this.botId = botId; + this.senderId = senderId; + this.senderName = senderName; + this.message = message; + this.botFriend = botFriend; + } + + public long getBotId() { + return botId; + } + + public long getSenderId() { + return senderId; + } + + public String getSenderName() { + return senderName; + } + + public String getMessage() { + return message; + } + + public BotFriend getBotFriend() { + return botFriend; + } +} diff --git a/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java b/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java new file mode 100644 index 0000000..278744f --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/event/BotGroupMessageEvent.java @@ -0,0 +1,52 @@ +package com.yaohun.mcbot.event; + +import com.yaohun.mcbot.data.BotGroup; +import net.md_5.bungee.api.plugin.Event; + +public class BotGroupMessageEvent extends Event { + + private long botId; + + private long groupId; + + private long senderId; + + private String senderName; + + private String message; + + private BotGroup botGroup; + + public BotGroupMessageEvent(long botId, long groupId, long senderId, String senderName, String message, BotGroup botGroup) { + this.botId = botId; + this.groupId = groupId; + this.senderId = senderId; + this.senderName = senderName; + this.message = message; + this.botGroup = botGroup; + } + + public long getBotId() { + return botId; + } + + public long getGroupId() { + return groupId; + } + + public long getSenderId() { + return senderId; + } + + public String getSenderName() { + return senderName; + } + + public String getMessage() { + return message; + } + + public BotGroup getBotGroup() { + return botGroup; + } +} diff --git a/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java b/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java new file mode 100644 index 0000000..1480df9 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/listsener/FriendListener.java @@ -0,0 +1,84 @@ +package com.yaohun.mcbot.listsener; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.api.McBotAPI; +import com.yaohun.mcbot.config.Config; +import com.yaohun.mcbot.data.sql.SQLIO; +import com.yaohun.mcbot.event.BotFriendMessageEvent; +import com.yaohun.mcbot.event.BotGroupMessageEvent; +import com.yaohun.mcbot.util.CDTimeAPI; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; + +import java.util.concurrent.TimeUnit; + +public class FriendListener implements Listener { + + @EventHandler + public void onGroupChat(BotFriendMessageEvent e) { + long senderId = e.getSenderId(); + if(CDTimeAPI.isCD(senderId,"groupBotCd")){ + return; + } + CDTimeAPI.addPlayerCD(senderId,"groupBotCd",1000 * 5); + String message = e.getMessage(); + if(message.contains("/功能") || message.contains("/菜单")) { + String to_message = "/查询 - 查看绑定信息\n" + + "/在线 - 查看当前在线人数\n" + + "/延迟 [玩家] - 网络延迟查看\n" + + "/强制下线 - 让账号在游戏中强制退出"; + e.getBotFriend().sendMessage(to_message); + }else if(e.getMessage().equalsIgnoreCase("/在线")) { + int onlineplayer = BungeeCord.getInstance().getOnlineCount(); + String msg = Config.getMessage("checkServerOnline").replace("%count%",String.valueOf(onlineplayer)); + e.getBotFriend().sendMessage(msg); + }else if(message.contains("/查询")) { + // 获取这个发送者的QQ号 + String userName = SQLIO.getUserName(String.valueOf(e.getSenderId())); + if(userName == null){ + String msg = Config.getMessage("notBoundYet"); + e.getBotFriend().sendMessage(msg); + return; + } + String msg = Config.getMessage("querySuccessful"); + e.getBotFriend().sendMessage(msg.replace("%user%", userName)); + }else if(e.getMessage().contains("/延迟 ")){ + // 获取玩家名字 + String playName = e.getMessage().replace("/延迟 ","").replace(" ",""); + ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playName); + if (player == null) { + String msg = Config.getMessage("offlinePlayer").replace("%user%", playName); + e.getBotFriend().sendMessage(msg); + return; + } + String msg = Config.getMessage("checkPlayerPing").replace("%user%", playName); + msg = msg.replace("%ping%",String.valueOf(player.getPing())); + e.getBotFriend().sendMessage(msg); + }else if(e.getMessage().contains("/强制下线")){ + // 获取这个发送者的QQ号 + String userName = SQLIO.getUserName(String.valueOf(e.getSenderId())); + if(userName == null){ + String msg = Config.getMessage("unboundAccount"); + e.getBotFriend().sendMessage(msg); + return; + } + ProxiedPlayer player = ProxyServer.getInstance().getPlayer(userName); + if (player == null) { + String msg = Config.getMessage("offlinePlayerError").replace("%user%", userName); + e.getBotFriend().sendMessage(msg); + return; + } + ProxyServer.getInstance().getScheduler().schedule(McBot.inst(), + new Runnable() { + public void run() { + player.disconnect("§c号主已进行强制下线"); + String msg = Config.getMessage("kickOnlinPlayer").replace("%user%",userName); + e.getBotFriend().sendMessage(msg); + } + }, 1L, TimeUnit.SECONDS); + } + } +} diff --git a/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java b/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java new file mode 100644 index 0000000..62ec9a8 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/listsener/GroupListener.java @@ -0,0 +1,105 @@ +package com.yaohun.mcbot.listsener; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.api.McBotAPI; +import com.yaohun.mcbot.config.Config; +import com.yaohun.mcbot.data.BotFriend; +import com.yaohun.mcbot.data.sql.SQLIO; +import com.yaohun.mcbot.event.BotGroupMessageEvent; +import com.yaohun.mcbot.manage.CacheManager; +import com.yaohun.mcbot.util.CDTimeAPI; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; + +import java.util.concurrent.TimeUnit; + +public class GroupListener implements Listener { + + @EventHandler + public void onGroupChat(BotGroupMessageEvent e) { + if (Config.BOT_GroupAccounts.contains(e.getGroupId())) { + long senderId = e.getSenderId(); + String message = e.getMessage(); + if (message.contains("/在线")) { + if (CDTimeAPI.isCD(senderId, "groupBotCd")) { + return; + } + CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 10); + int onlineplayer = BungeeCord.getInstance().getOnlineCount(); + String msg = Config.getMessage("checkServerOnline").replace("%count%", String.valueOf(onlineplayer)); + e.getBotGroup().sendMessage(msg); + } else if (message.contains("/查询")) { + if (CDTimeAPI.isCD(senderId, "groupBotCd")) { + return; + } + CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 10); + // 获取这个发送者的QQ号 + String userName = SQLIO.getUserName(String.valueOf(e.getSenderId())); + if (userName == null) { + String msg = Config.getMessage("notBoundYet"); + e.getBotGroup().sendMessage(msg); + return; + } + String msg = Config.getMessage("querySuccessful"); + e.getBotGroup().sendMessage(msg.replace("%user%", userName)); + } else if (message.contains("/绑定 ")) { + if (CDTimeAPI.isCD(senderId, "groupBotCd")) { + return; + } + CDTimeAPI.addPlayerCD(senderId, "groupBotCd", 1000 * 5); + String code = Config.extractBindCode(message); + // 检测这个验证码是否存在缓存数据 + CacheManager.CodeData codeData = CacheManager.getCodeData(code); + if (codeData == null) { + String msg = Config.getMessage("codeError").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]"); + e.getBotGroup().sendMessage(msg); + return; + } + // 判断缓存数据中的QQ和这个发送验证码的QQ是否为同一个 + String snederID = String.valueOf(e.getSenderId()); + if (!codeData.getTencentQQ().equals(snederID)) { + String msg = Config.getMessage("codeExpired").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]"); + e.getBotGroup().sendMessage(msg); + return; + } + // 判断这个QQ号是否已绑定游戏账号 + String userName = SQLIO.getUserName(snederID); + if (userName != null) { + String msg = Config.getMessage("alreadyGroupBindUserName").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]"); + e.getBotGroup().sendMessage(msg.replace("%user%", userName)); + return; + } + // 判断这个游戏账号是否已绑定其他QQ号 + String playerName = codeData.getUserName(); + String userID = SQLIO.getUserID(playerName); + if (userID != null) { + String msg = Config.getMessage("alreadyGroupBindUserID").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]"); + e.getBotGroup().sendMessage(msg.replace("%qq%", userID)); + return; + } + // 删除缓存数据 + CacheManager.cleanUpTheCache(playerName, code); + ProxyServer.getInstance().getScheduler().schedule(McBot.inst(), + new Runnable() { + public void run() { + //设置数据库数据 + SQLIO.saveGroup(playerName, snederID); + // 向群里发送绑定成功消息 + String msg1 = Config.getMessage("botBindingSuccessful").replace("@{nickName}", "[CQ:at,qq=" + e.getSenderId() + ",name=@" + e.getSenderName() + "]"); + e.getBotGroup().sendMessage(msg1.replace("%user%", playerName)); + // 向游戏发送绑定成功消息 + ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerName); + if (player != null) { + String msg2 = Config.getMessage("playerBindingSuccessful").replace("%qq%", String.valueOf(senderId)); + player.sendMessage(Config.getMessage("prefix") + msg2); + } + } + }, 1L, TimeUnit.SECONDS); + } + } + } + +} diff --git a/src/main/java/com/yaohun/mcbot/manage/CacheManager.java b/src/main/java/com/yaohun/mcbot/manage/CacheManager.java new file mode 100644 index 0000000..c7d3ad2 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/manage/CacheManager.java @@ -0,0 +1,82 @@ +package com.yaohun.mcbot.manage; + +import com.yaohun.mcbot.McBot; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class CacheManager { + + public static HashMap codeDataMap = new HashMap<>(); + public static HashMap randomCodeMap = new HashMap<>(); + + public static void setRandomCodeData(String userName,String code){ + randomCodeMap.put(userName,code); + } + + public static String getRandomCodeData(String userName){ + if(randomCodeMap.containsKey(userName)){ + return randomCodeMap.get(userName); + } + return null; + } + + public static void setCodeDataMap(String randomCode,CacheManager.CodeData codeData){ + codeDataMap.put(randomCode,codeData); + } + + public static CodeData getCodeData(String randomCode){ + if(codeDataMap.containsKey(randomCode)){ + return codeDataMap.get(randomCode); + } + return null; + } + + public static void cleanUpTheCache(String playerName,String randomCode){ + codeDataMap.remove(playerName); + randomCodeMap.remove(randomCode); + } + + public static void checkingCleanUpData(){ + if(codeDataMap.isEmpty()){ + return; + } + codeDataMap.clear(); + McBot.inst().getLogger().info("[McBot] 定时清理缓存池..."); + } + + public static class CodeData { + private String randomCode; + private String userName; + private String tencentQQ; + private long recordingTime; + + public CodeData(String userName, String randomCode, String tencentQQ) { + this.userName = userName; + this.randomCode = randomCode; + this.tencentQQ = tencentQQ; + this.recordingTime = System.currentTimeMillis(); + } + + public String getUserName() { + return userName; + } + + public String getRandomCode() { + return randomCode; + } + + public String getTencentQQ() { + return tencentQQ; + } + + public boolean isRecordingTime() { + long nowTime = System.currentTimeMillis(); + if(nowTime - recordingTime > (1000 * 120)){ + return true; + } + return false; + } + } +} diff --git a/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java b/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java new file mode 100644 index 0000000..d73af00 --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/manage/WssReconnectManager.java @@ -0,0 +1,73 @@ +package com.yaohun.mcbot.manage; + +import com.yaohun.mcbot.McBot; +import com.yaohun.mcbot.client.QQWebSocketClient; +import net.md_5.bungee.api.ProxyServer; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +public class WssReconnectManager { + + private static final int MAX_RETRY = 5; + private static final long RETRY_DELAY_SECONDS = 15; + + private static boolean reconnecting = false; + + private static int retryCount = 0; + + private static QQWebSocketClient currentClient; + private static String serverUri; + private static McBot plugin; + + public static void initialize(McBot pluginInstance, String uri) { + plugin = pluginInstance; + serverUri = uri; + } + + public static void setClient(QQWebSocketClient client) { + currentClient = client; + } + + public static QQWebSocketClient getClient() { + return currentClient; + } + + public static void tryReconnect() { + if (reconnecting) return; + + if (currentClient != null && currentClient.isOpen()) return; + + if (retryCount >= MAX_RETRY) { + plugin.getLogger().severe("[Bot-WebSocket] 已达到最大重连次数,停止尝试"); + return; + } + + reconnecting = true; + retryCount++; + + ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { + try { + // 关闭旧连接 + if (currentClient != null && currentClient.isOpen()) { + plugin.getLogger().info("[Bot-WebSocket] 关闭旧连接..."); + currentClient.close(); + } + + plugin.getLogger().info("[Bot-WebSocket] 重新连接尝试 第<" + retryCount + ">次 ..."); + QQWebSocketClient newClient = new QQWebSocketClient(serverUri); + setClient(newClient); + newClient.connect(); + } catch (Exception e) { + plugin.getLogger().severe("[Bot-WebSocket] 重连失败: " + e.getMessage()); + e.printStackTrace(); + } finally { + reconnecting = false; + } + }, RETRY_DELAY_SECONDS, TimeUnit.SECONDS); + } + + public static void resetRetry() { + retryCount = 0; + } +} diff --git a/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java b/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java new file mode 100644 index 0000000..2739caa --- /dev/null +++ b/src/main/java/com/yaohun/mcbot/util/CDTimeAPI.java @@ -0,0 +1,72 @@ +package com.yaohun.mcbot.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.UUID; + +public class CDTimeAPI { + + private static HashSet cdData = new HashSet<>(); + + public static CDData getCDData(long tencentQQ) { + for (CDData cDData : cdData) { + if (cDData.getTencentQQ() == tencentQQ) { + return cDData; + } + } + CDData data = new CDData(tencentQQ); + cdData.add(data); + return data; + } + + public static void setPlayerCD(long tencentQQ, String key, long mills) { + CDData data = getCDData(tencentQQ); + long now = System.currentTimeMillis(); + data.setCD(key, now + mills); + } + + public static void addPlayerCD(long tencentQQ, String key, long mills) { + if(isCD(tencentQQ,key)){ + setPlayerCD(tencentQQ,key,getCD(tencentQQ,key) + mills); + } else { + setPlayerCD(tencentQQ,key,mills); + } + } + + public static long getCD(long tencentQQ, String key) { + CDData data = getCDData(tencentQQ); + long now = System.currentTimeMillis(); + return data.getCD(key) - now; + } + + public static boolean isCD(long tencentQQ, String key) { + return (getCD(tencentQQ, key) >= 0L); + } + + public static class CDData{ + private final long tencentQQ; + + private final HashMap cdTime; + + public CDData(long tencentQQ) { + this.tencentQQ = tencentQQ; + this.cdTime = new HashMap<>(); + } + + public long getTencentQQ() { + return tencentQQ; + } + + public void setCD(String key, long time) { + this.cdTime.put(key, time); + } + + public long getCD(String key) { + return this.cdTime.getOrDefault(key,-1L); + } + + public HashMap getCdTime() { + return cdTime; + } + } +} diff --git a/src/main/java/org/java_websocket/AbstractWebSocket.java b/src/main/java/org/java_websocket/AbstractWebSocket.java new file mode 100644 index 0000000..ae9ce18 --- /dev/null +++ b/src/main/java/org/java_websocket/AbstractWebSocket.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.util.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Base class for additional implementations for the server as well as the client + */ +public abstract class AbstractWebSocket extends WebSocketAdapter { + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(AbstractWebSocket.class); + + /** + * Attribute which allows you to deactivate the Nagle's algorithm + * + * @since 1.3.3 + */ + private boolean tcpNoDelay; + + /** + * Attribute which allows you to enable/disable the SO_REUSEADDR socket option. + * + * @since 1.3.5 + */ + private boolean reuseAddr; + + /** + * Attribute for a service that triggers lost connection checking + * + * @since 1.4.1 + */ + private ScheduledExecutorService connectionLostCheckerService; + /** + * Attribute for a task that checks for lost connections + * + * @since 1.4.1 + */ + private ScheduledFuture connectionLostCheckerFuture; + + /** + * Attribute for the lost connection check interval in nanoseconds + * + * @since 1.3.4 + */ + private long connectionLostTimeout = TimeUnit.SECONDS.toNanos(60); + + /** + * Attribute to keep track if the WebSocket Server/Client is running/connected + * + * @since 1.3.9 + */ + private boolean websocketRunning = false; + + /** + * Attribute to start internal threads as daemon + * + * @since 1.5.6 + */ + private boolean daemon = false; + + /** + * Attribute to sync on + */ + private final Object syncConnectionLost = new Object(); + + /** + * TCP receive buffer size that will be used for sockets (zero means use system default) + * + * @since 1.5.7 + */ + private int receiveBufferSize = 0; + + /** + * Used for internal buffer allocations when the socket buffer size is not specified. + */ + protected static int DEFAULT_READ_BUFFER_SIZE = 65536; + + /** + * Get the interval checking for lost connections Default is 60 seconds + * + * @return the interval in seconds + * @since 1.3.4 + */ + public int getConnectionLostTimeout() { + synchronized (syncConnectionLost) { + return (int) TimeUnit.NANOSECONDS.toSeconds(connectionLostTimeout); + } + } + + /** + * Setter for the interval checking for lost connections A value lower or equal 0 results in the + * check to be deactivated + * + * @param connectionLostTimeout the interval in seconds + * @since 1.3.4 + */ + public void setConnectionLostTimeout(int connectionLostTimeout) { + synchronized (syncConnectionLost) { + this.connectionLostTimeout = TimeUnit.SECONDS.toNanos(connectionLostTimeout); + if (this.connectionLostTimeout <= 0) { + log.trace("Connection lost timer stopped"); + cancelConnectionLostTimer(); + return; + } + if (this.websocketRunning) { + log.trace("Connection lost timer restarted"); + //Reset all the pings + try { + ArrayList connections = new ArrayList<>(getConnections()); + WebSocketImpl webSocketImpl; + for (WebSocket conn : connections) { + if (conn instanceof WebSocketImpl) { + webSocketImpl = (WebSocketImpl) conn; + webSocketImpl.updateLastPong(); + } + } + } catch (Exception e) { + log.error("Exception during connection lost restart", e); + } + restartConnectionLostTimer(); + } + } + } + + /** + * Stop the connection lost timer + * + * @since 1.3.4 + */ + protected void stopConnectionLostTimer() { + synchronized (syncConnectionLost) { + if (connectionLostCheckerService != null || connectionLostCheckerFuture != null) { + this.websocketRunning = false; + log.trace("Connection lost timer stopped"); + cancelConnectionLostTimer(); + } + } + } + + /** + * Start the connection lost timer + * + * @since 1.3.4 + */ + protected void startConnectionLostTimer() { + synchronized (syncConnectionLost) { + if (this.connectionLostTimeout <= 0) { + log.trace("Connection lost timer deactivated"); + return; + } + log.trace("Connection lost timer started"); + this.websocketRunning = true; + restartConnectionLostTimer(); + } + } + + /** + * This methods allows the reset of the connection lost timer in case of a changed parameter + * + * @since 1.3.4 + */ + private void restartConnectionLostTimer() { + cancelConnectionLostTimer(); + connectionLostCheckerService = Executors + .newSingleThreadScheduledExecutor(new NamedThreadFactory("WebSocketConnectionLostChecker", daemon)); + Runnable connectionLostChecker = new Runnable() { + + /** + * Keep the connections in a separate list to not cause deadlocks + */ + private ArrayList connections = new ArrayList<>(); + + @Override + public void run() { + connections.clear(); + try { + connections.addAll(getConnections()); + long minimumPongTime; + synchronized (syncConnectionLost) { + minimumPongTime = (long) (System.nanoTime() - (connectionLostTimeout * 1.5)); + } + for (WebSocket conn : connections) { + executeConnectionLostDetection(conn, minimumPongTime); + } + } catch (Exception e) { + //Ignore this exception + } + connections.clear(); + } + }; + + connectionLostCheckerFuture = connectionLostCheckerService + .scheduleAtFixedRate(connectionLostChecker, connectionLostTimeout, connectionLostTimeout, + TimeUnit.NANOSECONDS); + } + + /** + * Send a ping to the endpoint or close the connection since the other endpoint did not respond + * with a ping + * + * @param webSocket the websocket instance + * @param minimumPongTime the lowest/oldest allowable last pong time (in nanoTime) before we + * consider the connection to be lost + */ + private void executeConnectionLostDetection(WebSocket webSocket, long minimumPongTime) { + if (!(webSocket instanceof WebSocketImpl)) { + return; + } + WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket; + if (webSocketImpl.getLastPong() < minimumPongTime) { + log.trace("Closing connection due to no pong received: {}", webSocketImpl); + webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE, + "The connection was closed because the other endpoint did not respond with a pong in time. For more information check: https://github.com/TooTallNate/Java-WebSocket/wiki/Lost-connection-detection"); + } else { + if (webSocketImpl.isOpen()) { + webSocketImpl.sendPing(); + } else { + log.trace("Trying to ping a non open connection: {}", webSocketImpl); + } + } + } + + /** + * Getter to get all the currently available connections + * + * @return the currently available connections + * @since 1.3.4 + */ + protected abstract Collection getConnections(); + + /** + * Cancel any running timer for the connection lost detection + * + * @since 1.3.4 + */ + private void cancelConnectionLostTimer() { + if (connectionLostCheckerService != null) { + connectionLostCheckerService.shutdownNow(); + connectionLostCheckerService = null; + } + if (connectionLostCheckerFuture != null) { + connectionLostCheckerFuture.cancel(false); + connectionLostCheckerFuture = null; + } + } + + /** + * Tests if TCP_NODELAY is enabled. + * + * @return a boolean indicating whether or not TCP_NODELAY is enabled for new connections. + * @since 1.3.3 + */ + public boolean isTcpNoDelay() { + return tcpNoDelay; + } + + /** + * Setter for tcpNoDelay + *

+ * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) for new connections + * + * @param tcpNoDelay true to enable TCP_NODELAY, false to disable. + * @since 1.3.3 + */ + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = tcpNoDelay; + } + + /** + * Tests Tests if SO_REUSEADDR is enabled. + * + * @return a boolean indicating whether or not SO_REUSEADDR is enabled. + * @since 1.3.5 + */ + public boolean isReuseAddr() { + return reuseAddr; + } + + /** + * Setter for soReuseAddr + *

+ * Enable/disable SO_REUSEADDR for the socket + * + * @param reuseAddr whether to enable or disable SO_REUSEADDR + * @since 1.3.5 + */ + public void setReuseAddr(boolean reuseAddr) { + this.reuseAddr = reuseAddr; + } + + + /** + * Getter for daemon + * + * @return whether internal threads are spawned in daemon mode + * @since 1.5.6 + */ + public boolean isDaemon() { + return daemon; + } + + /** + * Setter for daemon + *

+ * Controls whether or not internal threads are spawned in daemon mode + * + * @since 1.5.6 + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + /** + * Returns the TCP receive buffer size that will be used for sockets (or zero, if not explicitly set). + * @see java.net.Socket#setReceiveBufferSize(int) + * + * @since 1.5.7 + */ + public int getReceiveBufferSize() { + return receiveBufferSize; + } + + /** + * Sets the TCP receive buffer size that will be used for sockets. + * If this is not explicitly set (or set to zero), the system default is used. + * @see java.net.Socket#setReceiveBufferSize(int) + * + * @since 1.5.7 + */ + public void setReceiveBufferSize(int receiveBufferSize) { + if (receiveBufferSize < 0) { + throw new IllegalArgumentException("buffer size < 0"); + } + this.receiveBufferSize = receiveBufferSize; + } + +} diff --git a/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java new file mode 100644 index 0000000..33e6ddc --- /dev/null +++ b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.SocketChannel; + +/** + * @deprecated + */ +@Deprecated +public class AbstractWrappedByteChannel implements WrappedByteChannel { + + private final ByteChannel channel; + + /** + * @deprecated + */ + @Deprecated + public AbstractWrappedByteChannel(ByteChannel towrap) { + this.channel = towrap; + } + + /** + * @deprecated + */ + @Deprecated + public AbstractWrappedByteChannel(WrappedByteChannel towrap) { + this.channel = towrap; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + return channel.read(dst); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + return channel.write(src); + } + + @Override + public boolean isNeedWrite() { + return channel instanceof WrappedByteChannel && ((WrappedByteChannel) channel).isNeedWrite(); + } + + @Override + public void writeMore() throws IOException { + if (channel instanceof WrappedByteChannel) { + ((WrappedByteChannel) channel).writeMore(); + } + + } + + @Override + public boolean isNeedRead() { + return channel instanceof WrappedByteChannel && ((WrappedByteChannel) channel).isNeedRead(); + + } + + @Override + public int readMore(ByteBuffer dst) throws IOException { + return channel instanceof WrappedByteChannel ? ((WrappedByteChannel) channel).readMore(dst) : 0; + } + + @Override + public boolean isBlocking() { + if (channel instanceof SocketChannel) { + return ((SocketChannel) channel).isBlocking(); + } else if (channel instanceof WrappedByteChannel) { + return ((WrappedByteChannel) channel).isBlocking(); + } + return false; + } + +} diff --git a/src/main/java/org/java_websocket/SSLSocketChannel.java b/src/main/java/org/java_websocket/SSLSocketChannel.java new file mode 100644 index 0000000..b402450 --- /dev/null +++ b/src/main/java/org/java_websocket/SSLSocketChannel.java @@ -0,0 +1,539 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ExecutorService; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import org.java_websocket.interfaces.ISSLChannel; +import org.java_websocket.util.ByteBufferUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * A class that represents an SSL/TLS peer, and can be extended to create a client or a server. + *

+ * It makes use of the JSSE framework, and specifically the {@link SSLEngine} logic, which is + * described by Oracle as "an advanced API, not appropriate for casual use", since it requires the + * user to implement much of the communication establishment procedure himself. More information + * about it can be found here: http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine + *

+ * {@link SSLSocketChannel} implements the handshake protocol, required to establish a connection + * between two peers, which is common for both client and server and provides the abstract {@link + * SSLSocketChannel#read(ByteBuffer)} and {@link SSLSocketChannel#write(ByteBuffer)} (String)} + * methods, that need to be implemented by the specific SSL/TLS peer that is going to extend this + * class. + * + * @author Alex Karnezis + *

+ * Modified by marci4 to allow the usage as a ByteChannel + *

+ * Permission for usage received at May 25, 2017 by Alex Karnezis + */ +public class SSLSocketChannel implements WrappedByteChannel, ByteChannel, ISSLChannel { + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(SSLSocketChannel.class); + + /** + * The underlying socket channel + */ + private final SocketChannel socketChannel; + + /** + * The engine which will be used for un-/wrapping of buffers + */ + private final SSLEngine engine; + + + /** + * Will contain this peer's application data in plaintext, that will be later encrypted using + * {@link SSLEngine#wrap(ByteBuffer, ByteBuffer)} and sent to the other peer. This buffer can + * typically be of any size, as long as it is large enough to contain this peer's outgoing + * messages. If this peer tries to send a message bigger than buffer's capacity a {@link + * BufferOverflowException} will be thrown. + */ + private ByteBuffer myAppData; + + /** + * Will contain this peer's encrypted data, that will be generated after {@link + * SSLEngine#wrap(ByteBuffer, ByteBuffer)} is applied on {@link SSLSocketChannel#myAppData}. It + * should be initialized using {@link SSLSession#getPacketBufferSize()}, which returns the size up + * to which, SSL/TLS packets will be generated from the engine under a session. All SSLEngine + * network buffers should be sized at least this large to avoid insufficient space problems when + * performing wrap and unwrap calls. + */ + private ByteBuffer myNetData; + + /** + * Will contain the other peer's (decrypted) application data. It must be large enough to hold the + * application data from any peer. Can be initialized with {@link SSLSession#getApplicationBufferSize()} + * for an estimation of the other peer's application data and should be enlarged if this size is + * not enough. + */ + private ByteBuffer peerAppData; + + /** + * Will contain the other peer's encrypted data. The SSL/TLS protocols specify that + * implementations should produce packets containing at most 16 KB of plaintext, so a buffer sized + * to this value should normally cause no capacity problems. However, some implementations violate + * the specification and generate large records up to 32 KB. If the {@link + * SSLEngine#unwrap(ByteBuffer, ByteBuffer)} detects large inbound packets, the buffer sizes + * returned by SSLSession will be updated dynamically, so the this peer should check for overflow + * conditions and enlarge the buffer using the session's (updated) buffer size. + */ + private ByteBuffer peerNetData; + + /** + * Will be used to execute tasks that may emerge during handshake in parallel with the server's + * main thread. + */ + private ExecutorService executor; + + + public SSLSocketChannel(SocketChannel inputSocketChannel, SSLEngine inputEngine, + ExecutorService inputExecutor, SelectionKey key) throws IOException { + if (inputSocketChannel == null || inputEngine == null || executor == inputExecutor) { + throw new IllegalArgumentException("parameter must not be null"); + } + + this.socketChannel = inputSocketChannel; + this.engine = inputEngine; + this.executor = inputExecutor; + myNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); + peerNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); + this.engine.beginHandshake(); + if (doHandshake()) { + if (key != null) { + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + } + } else { + try { + socketChannel.close(); + } catch (IOException e) { + log.error("Exception during the closing of the channel", e); + } + } + } + + @Override + public synchronized int read(ByteBuffer dst) throws IOException { + if (!dst.hasRemaining()) { + return 0; + } + if (peerAppData.hasRemaining()) { + peerAppData.flip(); + return ByteBufferUtils.transferByteBuffer(peerAppData, dst); + } + peerNetData.compact(); + + int bytesRead = socketChannel.read(peerNetData); + /* + * If bytesRead are 0 put we still have some data in peerNetData still to an unwrap (for testcase 1.1.6) + */ + if (bytesRead > 0 || peerNetData.hasRemaining()) { + peerNetData.flip(); + while (peerNetData.hasRemaining()) { + peerAppData.compact(); + SSLEngineResult result; + try { + result = engine.unwrap(peerNetData, peerAppData); + } catch (SSLException e) { + log.error("SSLException during unwrap", e); + throw e; + } + switch (result.getStatus()) { + case OK: + peerAppData.flip(); + return ByteBufferUtils.transferByteBuffer(peerAppData, dst); + case BUFFER_UNDERFLOW: + peerAppData.flip(); + return ByteBufferUtils.transferByteBuffer(peerAppData, dst); + case BUFFER_OVERFLOW: + peerAppData = enlargeApplicationBuffer(peerAppData); + return read(dst); + case CLOSED: + closeConnection(); + dst.clear(); + return -1; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + } else if (bytesRead < 0) { + handleEndOfStream(); + } + ByteBufferUtils.transferByteBuffer(peerAppData, dst); + return bytesRead; + } + + @Override + public synchronized int write(ByteBuffer output) throws IOException { + int num = 0; + while (output.hasRemaining()) { + // The loop has a meaning for (outgoing) messages larger than 16KB. + // Every wrap call will remove 16KB from the original message and send it to the remote peer. + myNetData.clear(); + SSLEngineResult result = engine.wrap(output, myNetData); + switch (result.getStatus()) { + case OK: + myNetData.flip(); + while (myNetData.hasRemaining()) { + num += socketChannel.write(myNetData); + } + break; + case BUFFER_OVERFLOW: + myNetData = enlargePacketBuffer(myNetData); + break; + case BUFFER_UNDERFLOW: + throw new SSLException( + "Buffer underflow occurred after a wrap. I don't think we should ever get here."); + case CLOSED: + closeConnection(); + return 0; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + } + return num; + } + + /** + * Implements the handshake protocol between two peers, required for the establishment of the + * SSL/TLS connection. During the handshake, encryption configuration information - such as the + * list of available cipher suites - will be exchanged and if the handshake is successful will + * lead to an established SSL/TLS session. + *

+ *

+ * A typical handshake will usually contain the following steps: + *

+ *

    + *
  • 1. wrap: ClientHello
  • + *
  • 2. unwrap: ServerHello/Cert/ServerHelloDone
  • + *
  • 3. wrap: ClientKeyExchange
  • + *
  • 4. wrap: ChangeCipherSpec
  • + *
  • 5. wrap: Finished
  • + *
  • 6. unwrap: ChangeCipherSpec
  • + *
  • 7. unwrap: Finished
  • + *
+ *

+ * Handshake is also used during the end of the session, in order to properly close the connection between the two peers. + * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for + * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message + * from his peer and then enter the handshake procedure to send his own CLOSE message as well. + * + * @return True if the connection handshake was successful or false if an error occurred. + * @throws IOException - if an error occurs during read/write to the socket channel. + */ + private boolean doHandshake() throws IOException { + SSLEngineResult result; + HandshakeStatus handshakeStatus; + + // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer + // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less + // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers + // to be used for the handshake, while keeping client's buffers at the same size. + int appBufferSize = engine.getSession().getApplicationBufferSize(); + myAppData = ByteBuffer.allocate(appBufferSize); + peerAppData = ByteBuffer.allocate(appBufferSize); + myNetData.clear(); + peerNetData.clear(); + + handshakeStatus = engine.getHandshakeStatus(); + boolean handshakeComplete = false; + while (!handshakeComplete) { + switch (handshakeStatus) { + case FINISHED: + handshakeComplete = !this.peerNetData.hasRemaining(); + if (handshakeComplete) { + return true; + } + socketChannel.write(this.peerNetData); + break; + case NEED_UNWRAP: + if (socketChannel.read(peerNetData) < 0) { + if (engine.isInboundDone() && engine.isOutboundDone()) { + return false; + } + try { + engine.closeInbound(); + } catch (SSLException e) { + //Ignore, can't do anything against this exception + } + engine.closeOutbound(); + // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client. + handshakeStatus = engine.getHandshakeStatus(); + break; + } + peerNetData.flip(); + try { + result = engine.unwrap(peerNetData, peerAppData); + peerNetData.compact(); + handshakeStatus = result.getHandshakeStatus(); + } catch (SSLException sslException) { + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + switch (result.getStatus()) { + case OK: + break; + case BUFFER_OVERFLOW: + // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap. + peerAppData = enlargeApplicationBuffer(peerAppData); + break; + case BUFFER_UNDERFLOW: + // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data. + peerNetData = handleBufferUnderflow(peerNetData); + break; + case CLOSED: + if (engine.isOutboundDone()) { + return false; + } else { + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + break; + case NEED_WRAP: + myNetData.clear(); + try { + result = engine.wrap(myAppData, myNetData); + handshakeStatus = result.getHandshakeStatus(); + } catch (SSLException sslException) { + engine.closeOutbound(); + handshakeStatus = engine.getHandshakeStatus(); + break; + } + switch (result.getStatus()) { + case OK: + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + break; + case BUFFER_OVERFLOW: + // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap. + // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed + // to produce messages smaller or equal to that, but a general handling would be the following: + myNetData = enlargePacketBuffer(myNetData); + break; + case BUFFER_UNDERFLOW: + throw new SSLException( + "Buffer underflow occurred after a wrap. I don't think we should ever get here."); + case CLOSED: + try { + myNetData.flip(); + while (myNetData.hasRemaining()) { + socketChannel.write(myNetData); + } + // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read. + peerNetData.clear(); + } catch (Exception e) { + handshakeStatus = engine.getHandshakeStatus(); + } + break; + default: + throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); + } + break; + case NEED_TASK: + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + executor.execute(task); + } + handshakeStatus = engine.getHandshakeStatus(); + break; + + case NOT_HANDSHAKING: + break; + default: + throw new IllegalStateException("Invalid SSL status: " + handshakeStatus); + } + } + + return true; + + } + + /** + * Enlarging a packet buffer (peerNetData or myNetData) + * + * @param buffer the buffer to enlarge + * @return the enlarged buffer + */ + private ByteBuffer enlargePacketBuffer(ByteBuffer buffer) { + return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize()); + } + + /** + * Enlarging a packet buffer (peerAppData or myAppData) + * + * @param buffer the buffer to enlarge + * @return the enlarged buffer + */ + private ByteBuffer enlargeApplicationBuffer(ByteBuffer buffer) { + return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize()); + } + + /** + * Compares sessionProposedCapacity with buffer's capacity. If buffer's capacity is + * smaller, returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer + * with capacity twice the size of the initial one. + * + * @param buffer - the buffer to be enlarged. + * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link + * SSLSession}. + * @return A new buffer with a larger capacity. + */ + private ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) { + if (sessionProposedCapacity > buffer.capacity()) { + buffer = ByteBuffer.allocate(sessionProposedCapacity); + } else { + buffer = ByteBuffer.allocate(buffer.capacity() * 2); + } + return buffer; + } + + /** + * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already + * filled, and if there is no space problem will return the same buffer, so the client tries to + * read again. If the buffer is already filled will try to enlarge the buffer either to session's + * proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so + * the buffer will always be a peerNetData buffer. + * + * @param buffer - will always be peerNetData buffer. + * @return The same buffer if there is no space problem or a new buffer with the same data but + * more space. + */ + private ByteBuffer handleBufferUnderflow(ByteBuffer buffer) { + if (engine.getSession().getPacketBufferSize() < buffer.limit()) { + return buffer; + } else { + ByteBuffer replaceBuffer = enlargePacketBuffer(buffer); + buffer.flip(); + replaceBuffer.put(buffer); + return replaceBuffer; + } + } + + /** + * This method should be called when this peer wants to explicitly close the connection or when a + * close message has arrived from the other peer, in order to provide an orderly shutdown. + *

+ * It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close + * message and sets {@link SSLEngine} to the NEED_WRAP state. Then, it delegates the + * exchange of close messages to the handshake method and finally, it closes socket channel. + * + * @throws IOException if an I/O error occurs to the socket channel. + */ + private void closeConnection() throws IOException { + engine.closeOutbound(); + try { + doHandshake(); + } catch (IOException e) { + //Just ignore this exception since we are closing the connection already + } + socketChannel.close(); + } + + /** + * In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link + * (socket channel) is severed before close messages are exchanged. This may happen by getting an + * -1 or {@link IOException} when trying to read from the socket channel, or an {@link + * IOException} when trying to write to it. In both cases {@link SSLEngine#closeInbound()} should + * be called and then try to follow the standard procedure. + * + * @throws IOException if an I/O error occurs to the socket channel. + */ + private void handleEndOfStream() throws IOException { + try { + engine.closeInbound(); + } catch (Exception e) { + log.error( + "This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream."); + } + closeConnection(); + } + + @Override + public boolean isNeedWrite() { + return false; + } + + @Override + public void writeMore() throws IOException { + //Nothing to do since we write out all the data in a while loop + } + + @Override + public boolean isNeedRead() { + return peerNetData.hasRemaining() || peerAppData.hasRemaining(); + } + + @Override + public int readMore(ByteBuffer dst) throws IOException { + return read(dst); + } + + @Override + public boolean isBlocking() { + return socketChannel.isBlocking(); + } + + + @Override + public boolean isOpen() { + return socketChannel.isOpen(); + } + + @Override + public void close() throws IOException { + closeConnection(); + } + + @Override + public SSLEngine getSSLEngine() { + return engine; + } +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/SSLSocketChannel2.java b/src/main/java/org/java_websocket/SSLSocketChannel2.java new file mode 100644 index 0000000..3b9c778 --- /dev/null +++ b/src/main/java/org/java_websocket/SSLSocketChannel2.java @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.EOFException; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import org.java_websocket.interfaces.ISSLChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements the relevant portions of the SocketChannel interface with the SSLEngine wrapper. + */ +public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel, ISSLChannel { + + /** + * This object is used to feed the {@link SSLEngine}'s wrap and unwrap methods during the + * handshake phase. + **/ + protected static ByteBuffer emptybuffer = ByteBuffer.allocate(0); + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(SSLSocketChannel2.class); + + protected ExecutorService exec; + + protected List> tasks; + + /** + * raw payload incoming + */ + protected ByteBuffer inData; + /** + * encrypted data outgoing + */ + protected ByteBuffer outCrypt; + /** + * encrypted data incoming + */ + protected ByteBuffer inCrypt; + + /** + * the underlying channel + */ + protected SocketChannel socketChannel; + /** + * used to set interestOP SelectionKey.OP_WRITE for the underlying channel + */ + protected SelectionKey selectionKey; + + protected SSLEngine sslEngine; + protected SSLEngineResult readEngineResult; + protected SSLEngineResult writeEngineResult; + + /** + * Should be used to count the buffer allocations. But because of #190 where + * HandshakeStatus.FINISHED is not properly returned by nio wrap/unwrap this variable is used to + * check whether {@link #createBuffers(SSLSession)} needs to be called. + **/ + protected int bufferallocations = 0; + + public SSLSocketChannel2(SocketChannel channel, SSLEngine sslEngine, ExecutorService exec, + SelectionKey key) throws IOException { + if (channel == null || sslEngine == null || exec == null) { + throw new IllegalArgumentException("parameter must not be null"); + } + + this.socketChannel = channel; + this.sslEngine = sslEngine; + this.exec = exec; + + readEngineResult = writeEngineResult = new SSLEngineResult(Status.BUFFER_UNDERFLOW, + sslEngine.getHandshakeStatus(), 0, 0); // init to prevent NPEs + + tasks = new ArrayList>(3); + if (key != null) { + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + this.selectionKey = key; + } + createBuffers(sslEngine.getSession()); + // kick off handshake + socketChannel.write(wrap(emptybuffer));// initializes res + processHandshake(false); + } + + private void consumeFutureUninterruptible(Future f) { + try { + while (true) { + try { + f.get(); + break; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * This method will do whatever necessary to process the sslEngine handshake. Thats why it's + * called both from the {@link #read(ByteBuffer)} and {@link #write(ByteBuffer)} + **/ + private synchronized void processHandshake(boolean isReading) throws IOException { + if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) { + return; // since this may be called either from a reading or a writing thread and because this method is synchronized it is necessary to double check if we are still handshaking. + } + if (!tasks.isEmpty()) { + Iterator> it = tasks.iterator(); + while (it.hasNext()) { + Future f = it.next(); + if (f.isDone()) { + it.remove(); + } else { + if (isBlocking()) { + consumeFutureUninterruptible(f); + } + return; + } + } + } + + if (isReading && sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) { + if (!isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) { + inCrypt.compact(); + int read = socketChannel.read(inCrypt); + if (read == -1) { + throw new IOException("connection closed unexpectedly by peer"); + } + inCrypt.flip(); + } + inData.compact(); + unwrap(); + if (readEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { + createBuffers(sslEngine.getSession()); + return; + } + } + consumeDelegatedTasks(); + if (tasks.isEmpty() + || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) { + socketChannel.write(wrap(emptybuffer)); + if (writeEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { + createBuffers(sslEngine.getSession()); + return; + } + } + assert (sslEngine.getHandshakeStatus() + != HandshakeStatus.NOT_HANDSHAKING);// this function could only leave NOT_HANDSHAKING after createBuffers was called unless #190 occurs which means that nio wrap/unwrap never return HandshakeStatus.FINISHED + + bufferallocations = 1; // look at variable declaration why this line exists and #190. Without this line buffers would not be be recreated when #190 AND a rehandshake occur. + } + + private synchronized ByteBuffer wrap(ByteBuffer b) throws SSLException { + outCrypt.compact(); + writeEngineResult = sslEngine.wrap(b, outCrypt); + outCrypt.flip(); + return outCrypt; + } + + /** + * performs the unwrap operation by unwrapping from {@link #inCrypt} to {@link #inData} + **/ + private synchronized ByteBuffer unwrap() throws SSLException { + int rem; + //There are some ssl test suites, which get around the selector.select() call, which cause an infinite unwrap and 100% cpu usage (see #459 and #458) + if (readEngineResult.getStatus() == Status.CLOSED + && sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) { + try { + close(); + } catch (IOException e) { + //Not really interesting + } + } + do { + rem = inData.remaining(); + readEngineResult = sslEngine.unwrap(inCrypt, inData); + } while (readEngineResult.getStatus() == Status.OK && (rem != inData.remaining() + || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP)); + inData.flip(); + return inData; + } + + protected void consumeDelegatedTasks() { + Runnable task; + while ((task = sslEngine.getDelegatedTask()) != null) { + tasks.add(exec.submit(task)); + // task.run(); + } + } + + protected void createBuffers(SSLSession session) { + saveCryptedData(); // save any remaining data in inCrypt + int netBufferMax = session.getPacketBufferSize(); + int appBufferMax = Math.max(session.getApplicationBufferSize(), netBufferMax); + + if (inData == null) { + inData = ByteBuffer.allocate(appBufferMax); + outCrypt = ByteBuffer.allocate(netBufferMax); + inCrypt = ByteBuffer.allocate(netBufferMax); + } else { + if (inData.capacity() != appBufferMax) { + inData = ByteBuffer.allocate(appBufferMax); + } + if (outCrypt.capacity() != netBufferMax) { + outCrypt = ByteBuffer.allocate(netBufferMax); + } + if (inCrypt.capacity() != netBufferMax) { + inCrypt = ByteBuffer.allocate(netBufferMax); + } + } + if (inData.remaining() != 0 && log.isTraceEnabled()) { + log.trace(new String(inData.array(), inData.position(), inData.remaining())); + } + inData.rewind(); + inData.flip(); + if (inCrypt.remaining() != 0 && log.isTraceEnabled()) { + log.trace(new String(inCrypt.array(), inCrypt.position(), inCrypt.remaining())); + } + inCrypt.rewind(); + inCrypt.flip(); + outCrypt.rewind(); + outCrypt.flip(); + bufferallocations++; + } + + public int write(ByteBuffer src) throws IOException { + if (!isHandShakeComplete()) { + processHandshake(false); + return 0; + } + // assert(bufferallocations > 1); // see #190 + // if(bufferallocations <= 1) { + // createBuffers(sslEngine.getSession()); + // } + int num = socketChannel.write(wrap(src)); + if (writeEngineResult.getStatus() == Status.CLOSED) { + throw new EOFException("Connection is closed"); + } + return num; + + } + + /** + * Blocks when in blocking mode until at least one byte has been decoded.
When not in blocking + * mode 0 may be returned. + * + * @return the number of bytes read. + **/ + public int read(ByteBuffer dst) throws IOException { + tryRestoreCryptedData(); + while (true) { + if (!dst.hasRemaining()) { + return 0; + } + if (!isHandShakeComplete()) { + if (isBlocking()) { + while (!isHandShakeComplete()) { + processHandshake(true); + } + } else { + processHandshake(true); + if (!isHandShakeComplete()) { + return 0; + } + } + } + // assert(bufferallocations > 1); // see #190 + // if (bufferallocations <= 1) { + // createBuffers(sslEngine.getSession()); + // } + + /* 1. When "dst" is smaller than "inData" readRemaining will fill "dst" with data decoded in a previous read call. + * 2. When "inCrypt" contains more data than "inData" has remaining space, unwrap has to be called on more time(readRemaining) + */ + int purged = readRemaining(dst); + if (purged != 0) { + return purged; + } + + /* We only continue when we really need more data from the network. + * Thats the case if inData is empty or inCrypt holds to less data than necessary for decryption + */ + assert (inData.position() == 0); + inData.clear(); + + if (!inCrypt.hasRemaining()) { + inCrypt.clear(); + } else { + inCrypt.compact(); + } + + if (isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW) { + if (socketChannel.read(inCrypt) == -1) { + return -1; + } + } + inCrypt.flip(); + unwrap(); + + int transferred = transfereTo(inData, dst); + if (transferred == 0 && isBlocking()) { + continue; + } + return transferred; + } + } + + /** + * {@link #read(ByteBuffer)} may not be to leave all buffers(inData, inCrypt) + **/ + private int readRemaining(ByteBuffer dst) throws SSLException { + if (inData.hasRemaining()) { + return transfereTo(inData, dst); + } + if (!inData.hasRemaining()) { + inData.clear(); + } + tryRestoreCryptedData(); + // test if some bytes left from last read (e.g. BUFFER_UNDERFLOW) + if (inCrypt.hasRemaining()) { + unwrap(); + int amount = transfereTo(inData, dst); + if (readEngineResult.getStatus() == Status.CLOSED) { + return -1; + } + if (amount > 0) { + return amount; + } + } + return 0; + } + + public boolean isConnected() { + return socketChannel.isConnected(); + } + + public void close() throws IOException { + sslEngine.closeOutbound(); + sslEngine.getSession().invalidate(); + try { + if (socketChannel.isOpen()) { + socketChannel.write(wrap(emptybuffer)); + } + } finally { // in case socketChannel.write produce exception - channel will never close + socketChannel.close(); + } + } + + private boolean isHandShakeComplete() { + HandshakeStatus status = sslEngine.getHandshakeStatus(); + return status == HandshakeStatus.FINISHED + || status == HandshakeStatus.NOT_HANDSHAKING; + } + + public SelectableChannel configureBlocking(boolean b) throws IOException { + return socketChannel.configureBlocking(b); + } + + public boolean connect(SocketAddress remote) throws IOException { + return socketChannel.connect(remote); + } + + public boolean finishConnect() throws IOException { + return socketChannel.finishConnect(); + } + + public Socket socket() { + return socketChannel.socket(); + } + + public boolean isInboundDone() { + return sslEngine.isInboundDone(); + } + + @Override + public boolean isOpen() { + return socketChannel.isOpen(); + } + + @Override + public boolean isNeedWrite() { + return outCrypt.hasRemaining() + || !isHandShakeComplete(); // FIXME this condition can cause high cpu load during handshaking when network is slow + } + + @Override + public void writeMore() throws IOException { + write(outCrypt); + } + + @Override + public boolean isNeedRead() { + return saveCryptData != null || inData.hasRemaining() || (inCrypt.hasRemaining() + && readEngineResult.getStatus() != Status.BUFFER_UNDERFLOW + && readEngineResult.getStatus() != Status.CLOSED); + } + + @Override + public int readMore(ByteBuffer dst) throws SSLException { + return readRemaining(dst); + } + + private int transfereTo(ByteBuffer from, ByteBuffer to) { + int fremain = from.remaining(); + int toremain = to.remaining(); + if (fremain > toremain) { + // FIXME there should be a more efficient transfer method + int limit = Math.min(fremain, toremain); + for (int i = 0; i < limit; i++) { + to.put(from.get()); + } + return limit; + } else { + to.put(from); + return fremain; + } + + } + + @Override + public boolean isBlocking() { + return socketChannel.isBlocking(); + } + + @Override + public SSLEngine getSSLEngine() { + return sslEngine; + } + + + // to avoid complexities with inCrypt, extra unwrapped data after SSL handshake will be saved off in a byte array + // and the inserted back on first read + private byte[] saveCryptData = null; + + private void saveCryptedData() { + // did we find any extra data? + if (inCrypt != null && inCrypt.remaining() > 0) { + int saveCryptSize = inCrypt.remaining(); + saveCryptData = new byte[saveCryptSize]; + inCrypt.get(saveCryptData); + } + } + + private void tryRestoreCryptedData() { + // was there any extra data, then put into inCrypt and clean up + if (saveCryptData != null) { + inCrypt.clear(); + inCrypt.put(saveCryptData); + inCrypt.flip(); + saveCryptData = null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/SocketChannelIOHelper.java b/src/main/java/org/java_websocket/SocketChannelIOHelper.java new file mode 100644 index 0000000..9a6cfbc --- /dev/null +++ b/src/main/java/org/java_websocket/SocketChannelIOHelper.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import org.java_websocket.enums.Role; + +public class SocketChannelIOHelper { + + private SocketChannelIOHelper() { + throw new IllegalStateException("Utility class"); + } + + public static boolean read(final ByteBuffer buf, WebSocketImpl ws, ByteChannel channel) + throws IOException { + buf.clear(); + int read = channel.read(buf); + buf.flip(); + + if (read == -1) { + ws.eot(); + return false; + } + return read != 0; + } + + /** + * @param buf The ByteBuffer to read from + * @param ws The WebSocketImpl associated with the channels + * @param channel The channel to read from + * @return returns Whether there is more data left which can be obtained via {@link + * WrappedByteChannel#readMore(ByteBuffer)} + * @throws IOException May be thrown by {@link WrappedByteChannel#readMore(ByteBuffer)}# + * @see WrappedByteChannel#readMore(ByteBuffer) + **/ + public static boolean readMore(final ByteBuffer buf, WebSocketImpl ws, WrappedByteChannel channel) + throws IOException { + buf.clear(); + int read = channel.readMore(buf); + buf.flip(); + + if (read == -1) { + ws.eot(); + return false; + } + return channel.isNeedRead(); + } + + /** + * Returns whether the whole outQueue has been flushed + * + * @param ws The WebSocketImpl associated with the channels + * @param sockchannel The channel to write to + * @return returns Whether there is more data to write + * @throws IOException May be thrown by {@link WrappedByteChannel#writeMore()} + */ + public static boolean batch(WebSocketImpl ws, ByteChannel sockchannel) throws IOException { + if (ws == null) { + return false; + } + ByteBuffer buffer = ws.outQueue.peek(); + WrappedByteChannel c = null; + + if (buffer == null) { + if (sockchannel instanceof WrappedByteChannel) { + c = (WrappedByteChannel) sockchannel; + if (c.isNeedWrite()) { + c.writeMore(); + } + } + } else { + do { + // FIXME writing as much as possible is unfair!! + /*int written = */ + sockchannel.write(buffer); + if (buffer.remaining() > 0) { + return false; + } else { + ws.outQueue.poll(); // Buffer finished. Remove it. + buffer = ws.outQueue.peek(); + } + } while (buffer != null); + } + + if (ws.outQueue.isEmpty() && ws.isFlushAndClose() && ws.getDraft() != null + && ws.getDraft().getRole() != null && ws.getDraft().getRole() == Role.SERVER) { + ws.closeConnection(); + } + return c == null || !((WrappedByteChannel) sockchannel).isNeedWrite(); + } +} diff --git a/src/main/java/org/java_websocket/WebSocket.java b/src/main/java/org/java_websocket/WebSocket.java new file mode 100644 index 0000000..d515f63 --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocket.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Collection; +import javax.net.ssl.SSLSession; +import org.java_websocket.drafts.Draft; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.ReadyState; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.java_websocket.framing.Framedata; +import org.java_websocket.protocols.IProtocol; + +public interface WebSocket { + + /** + * sends the closing handshake. may be send in response to an other handshake. + * + * @param code the closing code + * @param message the closing message + */ + void close(int code, String message); + + /** + * sends the closing handshake. may be send in response to an other handshake. + * + * @param code the closing code + */ + void close(int code); + + /** + * Convenience function which behaves like close(CloseFrame.NORMAL) + */ + void close(); + + /** + * This will close the connection immediately without a proper close handshake. The code and the + * message therefore won't be transferred over the wire also they will be forwarded to + * onClose/onWebsocketClose. + * + * @param code the closing code + * @param message the closing message + **/ + void closeConnection(int code, String message); + + /** + * Send Text data to the other end. + * + * @param text the text data to send + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + void send(String text); + + /** + * Send Binary data (plain bytes) to the other end. + * + * @param bytes the binary data to send + * @throws IllegalArgumentException the data is null + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + void send(ByteBuffer bytes); + + /** + * Send Binary data (plain bytes) to the other end. + * + * @param bytes the byte array to send + * @throws IllegalArgumentException the data is null + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + void send(byte[] bytes); + + /** + * Send a frame to the other end + * + * @param framedata the frame to send to the other end + */ + void sendFrame(Framedata framedata); + + /** + * Send a collection of frames to the other end + * + * @param frames the frames to send to the other end + */ + void sendFrame(Collection frames); + + /** + * Send a ping to the other end + * + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + void sendPing(); + + /** + * Allows to send continuous/fragmented frames conveniently.
For more into on this frame type + * see http://tools.ietf.org/html/rfc6455#section-5.4
+ *

+ * If the first frame you send is also the last then it is not a fragmented frame and will + * received via onMessage instead of onFragmented even though it was send by this method. + * + * @param op This is only important for the first frame in the sequence. Opcode.TEXT, + * Opcode.BINARY are allowed. + * @param buffer The buffer which contains the payload. It may have no bytes remaining. + * @param fin true means the current frame is the last in the sequence. + **/ + void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin); + + /** + * Checks if the websocket has buffered data + * + * @return has the websocket buffered data + */ + boolean hasBufferedData(); + + /** + * Returns the address of the endpoint this socket is connected to, or {@code null} if it is + * unconnected. + * + * @return the remote socket address or null, if this socket is unconnected + */ + InetSocketAddress getRemoteSocketAddress(); + + /** + * Returns the address of the endpoint this socket is bound to, or {@code null} if it is not + * bound. + * + * @return the local socket address or null, if this socket is not bound + */ + InetSocketAddress getLocalSocketAddress(); + + /** + * Is the websocket in the state OPEN + * + * @return state equals ReadyState.OPEN + */ + boolean isOpen(); + + /** + * Is the websocket in the state CLOSING + * + * @return state equals ReadyState.CLOSING + */ + boolean isClosing(); + + /** + * Returns true when no further frames may be submitted
This happens before the socket + * connection is closed. + * + * @return true when no further frames may be submitted + */ + boolean isFlushAndClose(); + + /** + * Is the websocket in the state CLOSED + * + * @return state equals ReadyState.CLOSED + */ + boolean isClosed(); + + /** + * Getter for the draft + * + * @return the used draft + */ + Draft getDraft(); + + /** + * Retrieve the WebSocket 'ReadyState'. This represents the state of the connection. It returns a + * numerical value, as per W3C WebSockets specs. + * + * @return Returns '0 = CONNECTING', '1 = OPEN', '2 = CLOSING' or '3 = CLOSED' + */ + ReadyState getReadyState(); + + /** + * Returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2
+ * If the opening handshake has not yet happened it will return null. + * + * @return Returns the decoded path component of this URI. + **/ + String getResourceDescriptor(); + + /** + * Setter for an attachment on the socket connection. The attachment may be of any type. + * + * @param attachment The object to be attached to the user + * @param The type of the attachment + * @since 1.3.7 + **/ + void setAttachment(T attachment); + + /** + * Getter for the connection attachment. + * + * @param The type of the attachment + * @return Returns the user attachment + * @since 1.3.7 + **/ + T getAttachment(); + + /** + * Does this websocket use an encrypted (wss/ssl) or unencrypted (ws) connection + * + * @return true, if the websocket does use wss and therefore has a SSLSession + * @since 1.4.1 + */ + boolean hasSSLSupport(); + + /** + * Returns the ssl session of websocket, if ssl/wss is used for this instance. + * + * @return the ssl session of this websocket instance + * @throws IllegalArgumentException the underlying channel does not use ssl (use hasSSLSupport() + * to check) + * @since 1.4.1 + */ + SSLSession getSSLSession() throws IllegalArgumentException; + + /** + * Returns the used Sec-WebSocket-Protocol for this websocket connection + * + * @return the Sec-WebSocket-Protocol or null, if no draft available + * @throws IllegalArgumentException the underlying draft does not support a Sec-WebSocket-Protocol + * @since 1.5.2 + */ + IProtocol getProtocol(); +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/WebSocketAdapter.java b/src/main/java/org/java_websocket/WebSocketAdapter.java new file mode 100644 index 0000000..fdaf120 --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocketAdapter.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.PingFrame; +import org.java_websocket.framing.PongFrame; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.HandshakeImpl1Server; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; + +/** + * This class default implements all methods of the WebSocketListener that can be overridden + * optionally when advances functionalities is needed.
+ **/ +public abstract class WebSocketAdapter implements WebSocketListener { + + private PingFrame pingFrame; + + /** + * This default implementation does not do anything. Go ahead and overwrite it. + * + * @see WebSocketListener#onWebsocketHandshakeReceivedAsServer(WebSocket, + * Draft, ClientHandshake) + */ + @Override + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, + ClientHandshake request) throws InvalidDataException { + return new HandshakeImpl1Server(); + } + + @Override + public void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, + ServerHandshake response) throws InvalidDataException { + //To overwrite + } + + /** + * This default implementation does not do anything which will cause the connections to always + * progress. + * + * @see WebSocketListener#onWebsocketHandshakeSentAsClient(WebSocket, + * ClientHandshake) + */ + @Override + public void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request) + throws InvalidDataException { + //To overwrite + } + + /** + * This default implementation will send a pong in response to the received ping. The pong frame + * will have the same payload as the ping frame. + * + * @see WebSocketListener#onWebsocketPing(WebSocket, Framedata) + */ + @Override + public void onWebsocketPing(WebSocket conn, Framedata f) { + conn.sendFrame(new PongFrame((PingFrame) f)); + } + + /** + * This default implementation does not do anything. Go ahead and overwrite it. + * + * @see WebSocketListener#onWebsocketPong(WebSocket, Framedata) + */ + @Override + public void onWebsocketPong(WebSocket conn, Framedata f) { + //To overwrite + } + + /** + * Default implementation for onPreparePing, returns a (cached) PingFrame that has no application + * data. + * + * @param conn The WebSocket connection from which the ping frame will be sent. + * @return PingFrame to be sent. + * @see WebSocketListener#onPreparePing(WebSocket) + */ + @Override + public PingFrame onPreparePing(WebSocket conn) { + if (pingFrame == null) { + pingFrame = new PingFrame(); + } + return pingFrame; + } +} diff --git a/src/main/java/org/java_websocket/WebSocketFactory.java b/src/main/java/org/java_websocket/WebSocketFactory.java new file mode 100644 index 0000000..eed6e5a --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocketFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.util.List; +import org.java_websocket.drafts.Draft; + +public interface WebSocketFactory { + + /** + * Create a new Websocket with the provided listener, drafts and socket + * + * @param a The Listener for the WebsocketImpl + * @param d The draft which should be used + * @return A WebsocketImpl + */ + WebSocket createWebSocket(WebSocketAdapter a, Draft d); + + /** + * Create a new Websocket with the provided listener, drafts and socket + * + * @param a The Listener for the WebsocketImpl + * @param drafts The drafts which should be used + * @return A WebsocketImpl + */ + WebSocket createWebSocket(WebSocketAdapter a, List drafts); + +} diff --git a/src/main/java/org/java_websocket/WebSocketImpl.java b/src/main/java/org/java_websocket/WebSocketImpl.java new file mode 100644 index 0000000..3289aef --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocketImpl.java @@ -0,0 +1,915 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import javax.net.ssl.SSLSession; +import org.java_websocket.drafts.Draft; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.enums.CloseHandshakeType; +import org.java_websocket.enums.HandshakeState; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.ReadyState; +import org.java_websocket.enums.Role; +import org.java_websocket.exceptions.IncompleteHandshakeException; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.exceptions.LimitExceededException; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.PingFrame; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ClientHandshakeBuilder; +import org.java_websocket.handshake.Handshakedata; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; +import org.java_websocket.interfaces.ISSLChannel; +import org.java_websocket.protocols.IProtocol; +import org.java_websocket.server.WebSocketServer.WebSocketWorker; +import org.java_websocket.util.Charsetfunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents one end (client or server) of a single WebSocketImpl connection. Takes care of the + * "handshake" phase, then allows for easy sending of text frames, and receiving frames through an + * event-based model. + */ +public class WebSocketImpl implements WebSocket { + + /** + * The default port of WebSockets, as defined in the spec. If the nullary constructor is used, + * DEFAULT_PORT will be the port the WebSocketServer is binded to. Note that ports under 1024 + * usually require root permissions. + */ + public static final int DEFAULT_PORT = 80; + + /** + * The default wss port of WebSockets, as defined in the spec. If the nullary constructor is used, + * DEFAULT_WSS_PORT will be the port the WebSocketServer is binded to. Note that ports under 1024 + * usually require root permissions. + */ + public static final int DEFAULT_WSS_PORT = 443; + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(WebSocketImpl.class); + + /** + * Queue of buffers that need to be sent to the client. + */ + public final BlockingQueue outQueue; + /** + * Queue of buffers that need to be processed + */ + public final BlockingQueue inQueue; + /** + * The listener to notify of WebSocket events. + */ + private final WebSocketListener wsl; + + private SelectionKey key; + + /** + * the possibly wrapped channel object whose selection is controlled by {@link #key} + */ + private ByteChannel channel; + /** + * Helper variable meant to store the thread which ( exclusively ) triggers this objects decode + * method. + **/ + private WebSocketWorker workerThread; + /** + * When true no further frames may be submitted to be sent + */ + private boolean flushandclosestate = false; + + /** + * The current state of the connection + */ + private volatile ReadyState readyState = ReadyState.NOT_YET_CONNECTED; + + /** + * A list of drafts available for this websocket + */ + private List knownDrafts; + + /** + * The draft which is used by this websocket + */ + private Draft draft = null; + + /** + * The role which this websocket takes in the connection + */ + private Role role; + + /** + * the bytes of an incomplete received handshake + */ + private ByteBuffer tmpHandshakeBytes = ByteBuffer.allocate(0); + + /** + * stores the handshake sent by this websocket ( Role.CLIENT only ) + */ + private ClientHandshake handshakerequest = null; + + private String closemessage = null; + private Integer closecode = null; + private Boolean closedremotely = null; + + private String resourceDescriptor = null; + + /** + * Attribute, when the last pong was received + */ + private long lastPong = System.nanoTime(); + + /** + * Attribut to synchronize the write + */ + private final Object synchronizeWriteObject = new Object(); + + /** + * Attribute to store connection attachment + * + * @since 1.3.7 + */ + private Object attachment; + + /** + * Creates a websocket with server role + * + * @param listener The listener for this instance + * @param drafts The drafts which should be used + */ + public WebSocketImpl(WebSocketListener listener, List drafts) { + this(listener, (Draft) null); + this.role = Role.SERVER; + // draft.copyInstance will be called when the draft is first needed + if (drafts == null || drafts.isEmpty()) { + knownDrafts = new ArrayList<>(); + knownDrafts.add(new Draft_6455()); + } else { + knownDrafts = drafts; + } + } + + /** + * creates a websocket with client role + * + * @param listener The listener for this instance + * @param draft The draft which should be used + */ + public WebSocketImpl(WebSocketListener listener, Draft draft) { + // socket can be null because we want do be able to create the object without already having a bound channel + if (listener == null || (draft == null && role == Role.SERVER)) { + throw new IllegalArgumentException("parameters must not be null"); + } + this.outQueue = new LinkedBlockingQueue<>(); + inQueue = new LinkedBlockingQueue<>(); + this.wsl = listener; + this.role = Role.CLIENT; + if (draft != null) { + this.draft = draft.copyInstance(); + } + } + + /** + * Method to decode the provided ByteBuffer + * + * @param socketBuffer the ByteBuffer to decode + */ + public void decode(ByteBuffer socketBuffer) { + assert (socketBuffer.hasRemaining()); + if (log.isTraceEnabled()) { + log.trace("process({}): ({})", socketBuffer.remaining(), + (socketBuffer.remaining() > 1000 ? "too big to display" + : new String(socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining()))); + } + if (readyState != ReadyState.NOT_YET_CONNECTED) { + if (readyState == ReadyState.OPEN) { + decodeFrames(socketBuffer); + } + } else { + if (decodeHandshake(socketBuffer) && (!isClosing() && !isClosed())) { + assert (tmpHandshakeBytes.hasRemaining() != socketBuffer.hasRemaining() || !socketBuffer + .hasRemaining()); // the buffers will never have remaining bytes at the same time + if (socketBuffer.hasRemaining()) { + decodeFrames(socketBuffer); + } else if (tmpHandshakeBytes.hasRemaining()) { + decodeFrames(tmpHandshakeBytes); + } + } + } + } + + /** + * Returns whether the handshake phase has is completed. In case of a broken handshake this will + * be never the case. + **/ + private boolean decodeHandshake(ByteBuffer socketBufferNew) { + ByteBuffer socketBuffer; + if (tmpHandshakeBytes.capacity() == 0) { + socketBuffer = socketBufferNew; + } else { + if (tmpHandshakeBytes.remaining() < socketBufferNew.remaining()) { + ByteBuffer buf = ByteBuffer + .allocate(tmpHandshakeBytes.capacity() + socketBufferNew.remaining()); + tmpHandshakeBytes.flip(); + buf.put(tmpHandshakeBytes); + tmpHandshakeBytes = buf; + } + + tmpHandshakeBytes.put(socketBufferNew); + tmpHandshakeBytes.flip(); + socketBuffer = tmpHandshakeBytes; + } + socketBuffer.mark(); + try { + HandshakeState handshakestate; + try { + if (role == Role.SERVER) { + if (draft == null) { + for (Draft d : knownDrafts) { + d = d.copyInstance(); + try { + d.setParseMode(role); + socketBuffer.reset(); + Handshakedata tmphandshake = d.translateHandshake(socketBuffer); + if (!(tmphandshake instanceof ClientHandshake)) { + log.trace("Closing due to wrong handshake"); + closeConnectionDueToWrongHandshake( + new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "wrong http function")); + return false; + } + ClientHandshake handshake = (ClientHandshake) tmphandshake; + handshakestate = d.acceptHandshakeAsServer(handshake); + if (handshakestate == HandshakeState.MATCHED) { + resourceDescriptor = handshake.getResourceDescriptor(); + ServerHandshakeBuilder response; + try { + response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake); + } catch (InvalidDataException e) { + log.trace("Closing due to wrong handshake. Possible handshake rejection", e); + closeConnectionDueToWrongHandshake(e); + return false; + } catch (RuntimeException e) { + log.error("Closing due to internal server error", e); + wsl.onWebsocketError(this, e); + closeConnectionDueToInternalServerError(e); + return false; + } + write(d.createHandshake( + d.postProcessHandshakeResponseAsServer(handshake, response))); + draft = d; + open(handshake); + return true; + } + } catch (InvalidHandshakeException e) { + // go on with an other draft + } + } + if (draft == null) { + log.trace("Closing due to protocol error: no draft matches"); + closeConnectionDueToWrongHandshake( + new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "no draft matches")); + } + return false; + } else { + // special case for multiple step handshakes + Handshakedata tmphandshake = draft.translateHandshake(socketBuffer); + if (!(tmphandshake instanceof ClientHandshake)) { + log.trace("Closing due to protocol error: wrong http function"); + flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false); + return false; + } + ClientHandshake handshake = (ClientHandshake) tmphandshake; + handshakestate = draft.acceptHandshakeAsServer(handshake); + + if (handshakestate == HandshakeState.MATCHED) { + open(handshake); + return true; + } else { + log.trace("Closing due to protocol error: the handshake did finally not match"); + close(CloseFrame.PROTOCOL_ERROR, "the handshake did finally not match"); + } + return false; + } + } else if (role == Role.CLIENT) { + draft.setParseMode(role); + Handshakedata tmphandshake = draft.translateHandshake(socketBuffer); + if (!(tmphandshake instanceof ServerHandshake)) { + log.trace("Closing due to protocol error: wrong http function"); + flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", false); + return false; + } + ServerHandshake handshake = (ServerHandshake) tmphandshake; + handshakestate = draft.acceptHandshakeAsClient(handshakerequest, handshake); + if (handshakestate == HandshakeState.MATCHED) { + try { + wsl.onWebsocketHandshakeReceivedAsClient(this, handshakerequest, handshake); + } catch (InvalidDataException e) { + log.trace("Closing due to invalid data exception. Possible handshake rejection", e); + flushAndClose(e.getCloseCode(), e.getMessage(), false); + return false; + } catch (RuntimeException e) { + log.error("Closing since client was never connected", e); + wsl.onWebsocketError(this, e); + flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), false); + return false; + } + open(handshake); + return true; + } else { + log.trace("Closing due to protocol error: draft {} refuses handshake", draft); + close(CloseFrame.PROTOCOL_ERROR, "draft " + draft + " refuses handshake"); + } + } + } catch (InvalidHandshakeException e) { + log.trace("Closing due to invalid handshake", e); + close(e); + } + } catch (IncompleteHandshakeException e) { + if (tmpHandshakeBytes.capacity() == 0) { + socketBuffer.reset(); + int newsize = e.getPreferredSize(); + if (newsize == 0) { + newsize = socketBuffer.capacity() + 16; + } else { + assert (e.getPreferredSize() >= socketBuffer.remaining()); + } + tmpHandshakeBytes = ByteBuffer.allocate(newsize); + + tmpHandshakeBytes.put(socketBufferNew); + // tmpHandshakeBytes.flip(); + } else { + tmpHandshakeBytes.position(tmpHandshakeBytes.limit()); + tmpHandshakeBytes.limit(tmpHandshakeBytes.capacity()); + } + } + return false; + } + + private void decodeFrames(ByteBuffer socketBuffer) { + List frames; + try { + frames = draft.translateFrame(socketBuffer); + for (Framedata f : frames) { + log.trace("matched frame: {}", f); + draft.processFrame(this, f); + } + } catch (LimitExceededException e) { + if (e.getLimit() == Integer.MAX_VALUE) { + log.error("Closing due to invalid size of frame", e); + wsl.onWebsocketError(this, e); + } + close(e); + } catch (InvalidDataException e) { + log.error("Closing due to invalid data in frame", e); + wsl.onWebsocketError(this, e); + close(e); + } catch (VirtualMachineError | ThreadDeath | LinkageError e) { + log.error("Got fatal error during frame processing"); + throw e; + } catch (Error e) { + log.error("Closing web socket due to an error during frame processing"); + Exception exception = new Exception(e); + wsl.onWebsocketError(this, exception); + String errorMessage = "Got error " + e.getClass().getName(); + close(CloseFrame.UNEXPECTED_CONDITION, errorMessage); + } + } + + /** + * Close the connection if the received handshake was not correct + * + * @param exception the InvalidDataException causing this problem + */ + private void closeConnectionDueToWrongHandshake(InvalidDataException exception) { + write(generateHttpResponseDueToError(404)); + flushAndClose(exception.getCloseCode(), exception.getMessage(), false); + } + + /** + * Close the connection if there was a server error by a RuntimeException + * + * @param exception the RuntimeException causing this problem + */ + private void closeConnectionDueToInternalServerError(RuntimeException exception) { + write(generateHttpResponseDueToError(500)); + flushAndClose(CloseFrame.NEVER_CONNECTED, exception.getMessage(), false); + } + + /** + * Generate a simple response for the corresponding endpoint to indicate some error + * + * @param errorCode the http error code + * @return the complete response as ByteBuffer + */ + private ByteBuffer generateHttpResponseDueToError(int errorCode) { + String errorCodeDescription; + switch (errorCode) { + case 404: + errorCodeDescription = "404 WebSocket Upgrade Failure"; + break; + case 500: + default: + errorCodeDescription = "500 Internal Server Error"; + } + return ByteBuffer.wrap(Charsetfunctions.asciiBytes("HTTP/1.1 " + errorCodeDescription + + "\r\nContent-Type: text/html\r\nServer: TooTallNate Java-WebSocket\r\nContent-Length: " + + (48 + errorCodeDescription.length()) + "\r\n\r\n

" + + errorCodeDescription + "

")); + } + + public synchronized void close(int code, String message, boolean remote) { + if (readyState != ReadyState.CLOSING && readyState != ReadyState.CLOSED) { + if (readyState == ReadyState.OPEN) { + if (code == CloseFrame.ABNORMAL_CLOSE) { + assert (!remote); + readyState = ReadyState.CLOSING; + flushAndClose(code, message, false); + return; + } + if (draft.getCloseHandshakeType() != CloseHandshakeType.NONE) { + try { + if (!remote) { + try { + wsl.onWebsocketCloseInitiated(this, code, message); + } catch (RuntimeException e) { + wsl.onWebsocketError(this, e); + } + } + if (isOpen()) { + CloseFrame closeFrame = new CloseFrame(); + closeFrame.setReason(message); + closeFrame.setCode(code); + closeFrame.isValid(); + sendFrame(closeFrame); + } + } catch (InvalidDataException e) { + log.error("generated frame is invalid", e); + wsl.onWebsocketError(this, e); + flushAndClose(CloseFrame.ABNORMAL_CLOSE, "generated frame is invalid", false); + } + } + flushAndClose(code, message, remote); + } else if (code == CloseFrame.FLASHPOLICY) { + assert (remote); + flushAndClose(CloseFrame.FLASHPOLICY, message, true); + } else if (code == CloseFrame.PROTOCOL_ERROR) { // this endpoint found a PROTOCOL_ERROR + flushAndClose(code, message, remote); + } else { + flushAndClose(CloseFrame.NEVER_CONNECTED, message, false); + } + readyState = ReadyState.CLOSING; + tmpHandshakeBytes = null; + return; + } + } + + @Override + public void close(int code, String message) { + close(code, message, false); + } + + /** + * This will close the connection immediately without a proper close handshake. The code and the + * message therefore won't be transferred over the wire also they will be forwarded to + * onClose/onWebsocketClose. + * + * @param code the closing code + * @param message the closing message + * @param remote Indicates who "generated" code.
+ * true means that this endpoint received the code from + * the other endpoint.
false means this endpoint decided to send the given + * code,
+ * remote may also be true if this endpoint started the closing + * handshake since the other endpoint may not simply echo the code but + * close the connection the same time this endpoint does do but with an other + * code.
+ **/ + public synchronized void closeConnection(int code, String message, boolean remote) { + if (readyState == ReadyState.CLOSED) { + return; + } + //Methods like eot() call this method without calling onClose(). Due to that reason we have to adjust the ReadyState manually + if (readyState == ReadyState.OPEN) { + if (code == CloseFrame.ABNORMAL_CLOSE) { + readyState = ReadyState.CLOSING; + } + } + if (key != null) { + // key.attach( null ); //see issue #114 + key.cancel(); + } + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().equals("Broken pipe")) { + log.trace("Caught IOException: Broken pipe during closeConnection()", e); + } else { + log.error("Exception during channel.close()", e); + wsl.onWebsocketError(this, e); + } + } + } + try { + this.wsl.onWebsocketClose(this, code, message, remote); + } catch (RuntimeException e) { + + wsl.onWebsocketError(this, e); + } + if (draft != null) { + draft.reset(); + } + handshakerequest = null; + readyState = ReadyState.CLOSED; + } + + protected void closeConnection(int code, boolean remote) { + closeConnection(code, "", remote); + } + + public void closeConnection() { + if (closedremotely == null) { + throw new IllegalStateException("this method must be used in conjunction with flushAndClose"); + } + closeConnection(closecode, closemessage, closedremotely); + } + + public void closeConnection(int code, String message) { + closeConnection(code, message, false); + } + + public synchronized void flushAndClose(int code, String message, boolean remote) { + if (flushandclosestate) { + return; + } + closecode = code; + closemessage = message; + closedremotely = remote; + + flushandclosestate = true; + + wsl.onWriteDemand( + this); // ensures that all outgoing frames are flushed before closing the connection + try { + wsl.onWebsocketClosing(this, code, message, remote); + } catch (RuntimeException e) { + log.error("Exception in onWebsocketClosing", e); + wsl.onWebsocketError(this, e); + } + if (draft != null) { + draft.reset(); + } + handshakerequest = null; + } + + public void eot() { + if (readyState == ReadyState.NOT_YET_CONNECTED) { + closeConnection(CloseFrame.NEVER_CONNECTED, true); + } else if (flushandclosestate) { + closeConnection(closecode, closemessage, closedremotely); + } else if (draft.getCloseHandshakeType() == CloseHandshakeType.NONE) { + closeConnection(CloseFrame.NORMAL, true); + } else if (draft.getCloseHandshakeType() == CloseHandshakeType.ONEWAY) { + if (role == Role.SERVER) { + closeConnection(CloseFrame.ABNORMAL_CLOSE, true); + } else { + closeConnection(CloseFrame.NORMAL, true); + } + } else { + closeConnection(CloseFrame.ABNORMAL_CLOSE, true); + } + } + + @Override + public void close(int code) { + close(code, "", false); + } + + public void close(InvalidDataException e) { + close(e.getCloseCode(), e.getMessage(), false); + } + + /** + * Send Text data to the other end. + * + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + @Override + public void send(String text) { + if (text == null) { + throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl."); + } + send(draft.createFrames(text, role == Role.CLIENT)); + } + + /** + * Send Binary data (plain bytes) to the other end. + * + * @throws IllegalArgumentException the data is null + * @throws WebsocketNotConnectedException websocket is not yet connected + */ + @Override + public void send(ByteBuffer bytes) { + if (bytes == null) { + throw new IllegalArgumentException("Cannot send 'null' data to a WebSocketImpl."); + } + send(draft.createFrames(bytes, role == Role.CLIENT)); + } + + @Override + public void send(byte[] bytes) { + send(ByteBuffer.wrap(bytes)); + } + + private void send(Collection frames) { + if (!isOpen()) { + throw new WebsocketNotConnectedException(); + } + if (frames == null) { + throw new IllegalArgumentException(); + } + ArrayList outgoingFrames = new ArrayList<>(); + for (Framedata f : frames) { + log.trace("send frame: {}", f); + outgoingFrames.add(draft.createBinaryFrame(f)); + } + write(outgoingFrames); + } + + @Override + public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) { + send(draft.continuousFrame(op, buffer, fin)); + } + + @Override + public void sendFrame(Collection frames) { + send(frames); + } + + @Override + public void sendFrame(Framedata framedata) { + send(Collections.singletonList(framedata)); + } + + public void sendPing() throws NullPointerException { + // Gets a PingFrame from WebSocketListener(wsl) and sends it. + PingFrame pingFrame = wsl.onPreparePing(this); + if (pingFrame == null) { + throw new NullPointerException( + "onPreparePing(WebSocket) returned null. PingFrame to sent can't be null."); + } + sendFrame(pingFrame); + } + + @Override + public boolean hasBufferedData() { + return !this.outQueue.isEmpty(); + } + + public void startHandshake(ClientHandshakeBuilder handshakedata) + throws InvalidHandshakeException { + // Store the Handshake Request we are about to send + this.handshakerequest = draft.postProcessHandshakeRequestAsClient(handshakedata); + + resourceDescriptor = handshakedata.getResourceDescriptor(); + assert (resourceDescriptor != null); + + // Notify Listener + try { + wsl.onWebsocketHandshakeSentAsClient(this, this.handshakerequest); + } catch (InvalidDataException e) { + // Stop if the client code throws an exception + throw new InvalidHandshakeException("Handshake data rejected by client."); + } catch (RuntimeException e) { + log.error("Exception in startHandshake", e); + wsl.onWebsocketError(this, e); + throw new InvalidHandshakeException("rejected because of " + e); + } + + // Send + write(draft.createHandshake(this.handshakerequest)); + } + + private void write(ByteBuffer buf) { + log.trace("write({}): {}", buf.remaining(), + buf.remaining() > 1000 ? "too big to display" : new String(buf.array())); + + outQueue.add(buf); + wsl.onWriteDemand(this); + } + + /** + * Write a list of bytebuffer (frames in binary form) into the outgoing queue + * + * @param bufs the list of bytebuffer + */ + private void write(List bufs) { + synchronized (synchronizeWriteObject) { + for (ByteBuffer b : bufs) { + write(b); + } + } + } + + private void open(Handshakedata d) { + log.trace("open using draft: {}", draft); + readyState = ReadyState.OPEN; + updateLastPong(); + try { + wsl.onWebsocketOpen(this, d); + } catch (RuntimeException e) { + wsl.onWebsocketError(this, e); + } + } + + @Override + public boolean isOpen() { + return readyState == ReadyState.OPEN; + } + + @Override + public boolean isClosing() { + return readyState == ReadyState.CLOSING; + } + + @Override + public boolean isFlushAndClose() { + return flushandclosestate; + } + + @Override + public boolean isClosed() { + return readyState == ReadyState.CLOSED; + } + + @Override + public ReadyState getReadyState() { + return readyState; + } + + /** + * @param key the selection key of this implementation + */ + public void setSelectionKey(SelectionKey key) { + this.key = key; + } + + /** + * @return the selection key of this implementation + */ + public SelectionKey getSelectionKey() { + return key; + } + + @Override + public String toString() { + return super.toString(); // its nice to be able to set breakpoints here + } + + @Override + public InetSocketAddress getRemoteSocketAddress() { + return wsl.getRemoteSocketAddress(this); + } + + @Override + public InetSocketAddress getLocalSocketAddress() { + return wsl.getLocalSocketAddress(this); + } + + @Override + public Draft getDraft() { + return draft; + } + + @Override + public void close() { + close(CloseFrame.NORMAL); + } + + @Override + public String getResourceDescriptor() { + return resourceDescriptor; + } + + /** + * Getter for the last pong received + * + * @return the timestamp for the last received pong + */ + long getLastPong() { + return lastPong; + } + + /** + * Update the timestamp when the last pong was received + */ + public void updateLastPong() { + this.lastPong = System.nanoTime(); + } + + /** + * Getter for the websocket listener + * + * @return the websocket listener associated with this instance + */ + public WebSocketListener getWebSocketListener() { + return wsl; + } + + @Override + @SuppressWarnings("unchecked") + public T getAttachment() { + return (T) attachment; + } + + @Override + public boolean hasSSLSupport() { + return channel instanceof ISSLChannel; + } + + @Override + public SSLSession getSSLSession() { + if (!hasSSLSupport()) { + throw new IllegalArgumentException( + "This websocket uses ws instead of wss. No SSLSession available."); + } + return ((ISSLChannel) channel).getSSLEngine().getSession(); + } + + @Override + public IProtocol getProtocol() { + if (draft == null) { + return null; + } + if (!(draft instanceof Draft_6455)) { + throw new IllegalArgumentException("This draft does not support Sec-WebSocket-Protocol"); + } + return ((Draft_6455) draft).getProtocol(); + } + + @Override + public void setAttachment(T attachment) { + this.attachment = attachment; + } + + public ByteChannel getChannel() { + return channel; + } + + public void setChannel(ByteChannel channel) { + this.channel = channel; + } + + public WebSocketWorker getWorkerThread() { + return workerThread; + } + + public void setWorkerThread(WebSocketWorker workerThread) { + this.workerThread = workerThread; + } + + +} diff --git a/src/main/java/org/java_websocket/WebSocketListener.java b/src/main/java/org/java_websocket/WebSocketListener.java new file mode 100644 index 0000000..f0b21d5 --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocketListener.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.PingFrame; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.Handshakedata; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; + +/** + * Implemented by WebSocketClient and WebSocketServer. The methods within are + * called by WebSocket. Almost every method takes a first parameter conn which represents + * the source of the respective event. + */ +public interface WebSocketListener { + + /** + * Called on the server side when the socket connection is first established, and the WebSocket + * handshake has been received. This method allows to deny connections based on the received + * handshake.
By default this method only requires protocol compliance. + * + * @param conn The WebSocket related to this event + * @param draft The protocol draft the client uses to connect + * @param request The opening http message send by the client. Can be used to access additional + * fields like cookies. + * @return Returns an incomplete handshake containing all optional fields + * @throws InvalidDataException Throwing this exception will cause this handshake to be rejected + */ + ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, + ClientHandshake request) throws InvalidDataException; + + /** + * Called on the client side when the socket connection is first established, and the + * WebSocketImpl handshake response has been received. + * + * @param conn The WebSocket related to this event + * @param request The handshake initially send out to the server by this websocket. + * @param response The handshake the server sent in response to the request. + * @throws InvalidDataException Allows the client to reject the connection with the server in + * respect of its handshake response. + */ + void onWebsocketHandshakeReceivedAsClient(WebSocket conn, ClientHandshake request, + ServerHandshake response) throws InvalidDataException; + + /** + * Called on the client side when the socket connection is first established, and the + * WebSocketImpl handshake has just been sent. + * + * @param conn The WebSocket related to this event + * @param request The handshake sent to the server by this websocket + * @throws InvalidDataException Allows the client to stop the connection from progressing + */ + void onWebsocketHandshakeSentAsClient(WebSocket conn, ClientHandshake request) + throws InvalidDataException; + + /** + * Called when an entire text frame has been received. Do whatever you want here... + * + * @param conn The WebSocket instance this event is occurring on. + * @param message The UTF-8 decoded message that was received. + */ + void onWebsocketMessage(WebSocket conn, String message); + + /** + * Called when an entire binary frame has been received. Do whatever you want here... + * + * @param conn The WebSocket instance this event is occurring on. + * @param blob The binary message that was received. + */ + void onWebsocketMessage(WebSocket conn, ByteBuffer blob); + + /** + * Called after onHandshakeReceived returns true. Indicates that a complete + * WebSocket connection has been established, and we are ready to send/receive data. + * + * @param conn The WebSocket instance this event is occurring on. + * @param d The handshake of the websocket instance + */ + void onWebsocketOpen(WebSocket conn, Handshakedata d); + + /** + * Called after WebSocket#close is explicity called, or when the other end of the + * WebSocket connection is closed. + * + * @param ws The WebSocket instance this event is occurring on. + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + * @param remote Returns whether or not the closing of the connection was initiated by the remote + * host. + */ + void onWebsocketClose(WebSocket ws, int code, String reason, boolean remote); + + /** + * Called as soon as no further frames are accepted + * + * @param ws The WebSocket instance this event is occurring on. + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + * @param remote Returns whether or not the closing of the connection was initiated by the remote + * host. + */ + void onWebsocketClosing(WebSocket ws, int code, String reason, boolean remote); + + /** + * send when this peer sends a close handshake + * + * @param ws The WebSocket instance this event is occurring on. + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + */ + void onWebsocketCloseInitiated(WebSocket ws, int code, String reason); + + /** + * Called if an exception worth noting occurred. If an error causes the connection to fail onClose + * will be called additionally afterwards. + * + * @param conn The WebSocket instance this event is occurring on. + * @param ex The exception that occurred.
Might be null if the exception is not related to + * any specific connection. For example if the server port could not be bound. + */ + void onWebsocketError(WebSocket conn, Exception ex); + + /** + * Called a ping frame has been received. This method must send a corresponding pong by itself. + * + * @param conn The WebSocket instance this event is occurring on. + * @param f The ping frame. Control frames may contain payload. + */ + void onWebsocketPing(WebSocket conn, Framedata f); + + /** + * Called just before a ping frame is sent, in order to allow users to customize their ping frame + * data. + * + * @param conn The WebSocket connection from which the ping frame will be sent. + * @return PingFrame to be sent. + */ + PingFrame onPreparePing(WebSocket conn); + + /** + * Called when a pong frame is received. + * + * @param conn The WebSocket instance this event is occurring on. + * @param f The pong frame. Control frames may contain payload. + **/ + void onWebsocketPong(WebSocket conn, Framedata f); + + /** + * This method is used to inform the selector thread that there is data queued to be written to + * the socket. + * + * @param conn The WebSocket instance this event is occurring on. + */ + void onWriteDemand(WebSocket conn); + + /** + * @param conn The WebSocket instance this event is occurring on. + * @return Returns the address of the endpoint this socket is bound to. + * @see WebSocket#getLocalSocketAddress() + */ + InetSocketAddress getLocalSocketAddress(WebSocket conn); + + /** + * @param conn The WebSocket instance this event is occurring on. + * @return Returns the address of the endpoint this socket is connected to, or{@code null} if it + * is unconnected. + * @see WebSocket#getRemoteSocketAddress() + */ + InetSocketAddress getRemoteSocketAddress(WebSocket conn); +} diff --git a/src/main/java/org/java_websocket/WebSocketServerFactory.java b/src/main/java/org/java_websocket/WebSocketServerFactory.java new file mode 100644 index 0000000..825aa21 --- /dev/null +++ b/src/main/java/org/java_websocket/WebSocketServerFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.List; +import org.java_websocket.drafts.Draft; + +/** + * Interface to encapsulate the required methods for a websocket factory + */ +public interface WebSocketServerFactory extends WebSocketFactory { + + @Override + WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d); + + @Override + WebSocketImpl createWebSocket(WebSocketAdapter a, List drafts); + + /** + * Allows to wrap the SocketChannel( key.channel() ) to insert a protocol layer( like ssl or proxy + * authentication) beyond the ws layer. + * + * @param channel The SocketChannel to wrap + * @param key a SelectionKey of an open SocketChannel. + * @return The channel on which the read and write operations will be performed.
+ * @throws IOException may be thrown while writing on the channel + */ + ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException; + + /** + * Allows to shutdown the websocket factory for a clean shutdown + */ + void close(); +} diff --git a/src/main/java/org/java_websocket/WrappedByteChannel.java b/src/main/java/org/java_websocket/WrappedByteChannel.java new file mode 100644 index 0000000..8dee57d --- /dev/null +++ b/src/main/java/org/java_websocket/WrappedByteChannel.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + +public interface WrappedByteChannel extends ByteChannel { + + /** + * returns whether writeMore should be called write additional data. + * + * @return is a additional write needed + */ + boolean isNeedWrite(); + + /** + * Gets called when {@link #isNeedWrite()} ()} requires a additional rite + * + * @throws IOException may be thrown due to an error while writing + */ + void writeMore() throws IOException; + + /** + * returns whether readMore should be called to fetch data which has been decoded but not yet been + * returned. + * + * @return is a additional read needed + * @see #read(ByteBuffer) + * @see #readMore(ByteBuffer) + **/ + boolean isNeedRead(); + + /** + * This function does not read data from the underlying channel at all. It is just a way to fetch + * data which has already be received or decoded but was but was not yet returned to the user. + * This could be the case when the decoded data did not fit into the buffer the user passed to + * {@link #read(ByteBuffer)}. + * + * @param dst the destiny of the read + * @return the amount of remaining data + * @throws IOException when a error occurred during unwrapping + **/ + int readMore(ByteBuffer dst) throws IOException; + + /** + * This function returns the blocking state of the channel + * + * @return is the channel blocking + */ + boolean isBlocking(); +} diff --git a/src/main/java/org/java_websocket/client/DnsResolver.java b/src/main/java/org/java_websocket/client/DnsResolver.java new file mode 100644 index 0000000..ec9f17f --- /dev/null +++ b/src/main/java/org/java_websocket/client/DnsResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.client; + +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; + +/** + * Users may implement this interface to override the default DNS lookup offered by the OS. + * + * @since 1.4.1 + */ +public interface DnsResolver { + + /** + * Resolves the IP address for the given URI. + *

+ * This method should never return null. If it's not able to resolve the IP address then it should + * throw an UnknownHostException + * + * @param uri The URI to be resolved + * @return The resolved IP address + * @throws UnknownHostException if no IP address for the uri could be found. + */ + InetAddress resolve(URI uri) throws UnknownHostException; + +} diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java new file mode 100644 index 0000000..0e38326 --- /dev/null +++ b/src/main/java/org/java_websocket/client/WebSocketClient.java @@ -0,0 +1,1025 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.net.SocketFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.java_websocket.AbstractWebSocket; +import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.drafts.Draft; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.ReadyState; +import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.handshake.HandshakeImpl1Client; +import org.java_websocket.handshake.Handshakedata; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.protocols.IProtocol; + +/** + * A subclass must implement at least onOpen, onClose, and + * onMessage to be useful. At runtime the user is expected to establish a connection via + * {@link #connect()}, then receive events like {@link #onMessage(String)} via the overloaded + * methods and to {@link #send(String)} data to the server. + */ +public abstract class WebSocketClient extends AbstractWebSocket implements Runnable, WebSocket { + + /** + * The URI this channel is supposed to connect to. + */ + protected URI uri = null; + + /** + * The underlying engine + */ + private WebSocketImpl engine = null; + + /** + * The socket for this WebSocketClient + */ + private Socket socket = null; + + /** + * The SocketFactory for this WebSocketClient + * + * @since 1.4.0 + */ + private SocketFactory socketFactory = null; + + /** + * The used OutputStream + */ + private OutputStream ostream; + + /** + * The used proxy, if any + */ + private Proxy proxy = Proxy.NO_PROXY; + + /** + * The thread to write outgoing message + */ + private Thread writeThread; + + /** + * The thread to connect and read message + */ + private Thread connectReadThread; + + /** + * The draft to use + */ + private Draft draft; + + /** + * The additional headers to use + */ + private Map headers; + + /** + * The latch for connectBlocking() + */ + private CountDownLatch connectLatch = new CountDownLatch(1); + + /** + * The latch for closeBlocking() + */ + private CountDownLatch closeLatch = new CountDownLatch(1); + + /** + * The socket timeout value to be used in milliseconds. + */ + private int connectTimeout = 0; + + /** + * DNS resolver that translates a URI to an InetAddress + * + * @see InetAddress + * @since 1.4.1 + */ + private DnsResolver dnsResolver = null; + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The + * channel does not attampt to connect automatically. The connection will be established once you + * call connect. + * + * @param serverUri the server URI to connect to + */ + public WebSocketClient(URI serverUri) { + this(serverUri, new Draft_6455()); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The + * channel does not attampt to connect automatically. The connection will be established once you + * call connect. + * + * @param serverUri the server URI to connect to + * @param protocolDraft The draft which should be used for this connection + */ + public WebSocketClient(URI serverUri, Draft protocolDraft) { + this(serverUri, protocolDraft, null, 0); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The + * channel does not attampt to connect automatically. The connection will be established once you + * call connect. + * + * @param serverUri the server URI to connect to + * @param httpHeaders Additional HTTP-Headers + * @since 1.3.8 + */ + public WebSocketClient(URI serverUri, Map httpHeaders) { + this(serverUri, new Draft_6455(), httpHeaders); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The + * channel does not attampt to connect automatically. The connection will be established once you + * call connect. + * + * @param serverUri the server URI to connect to + * @param protocolDraft The draft which should be used for this connection + * @param httpHeaders Additional HTTP-Headers + * @since 1.3.8 + */ + public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders) { + this(serverUri, protocolDraft, httpHeaders, 0); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The + * channel does not attampt to connect automatically. The connection will be established once you + * call connect. + * + * @param serverUri the server URI to connect to + * @param protocolDraft The draft which should be used for this connection + * @param httpHeaders Additional HTTP-Headers + * @param connectTimeout The Timeout for the connection + */ + public WebSocketClient(URI serverUri, Draft protocolDraft, Map httpHeaders, + int connectTimeout) { + if (serverUri == null) { + throw new IllegalArgumentException(); + } else if (protocolDraft == null) { + throw new IllegalArgumentException("null as draft is permitted for `WebSocketServer` only!"); + } + this.uri = serverUri; + this.draft = protocolDraft; + this.dnsResolver = new DnsResolver() { + @Override + public InetAddress resolve(URI uri) throws UnknownHostException { + return InetAddress.getByName(uri.getHost()); + } + }; + if (httpHeaders != null) { + headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + headers.putAll(httpHeaders); + } + this.connectTimeout = connectTimeout; + setTcpNoDelay(false); + setReuseAddr(false); + this.engine = new WebSocketImpl(this, protocolDraft); + } + + /** + * Returns the URI that this WebSocketClient is connected to. + * + * @return the URI connected to + */ + public URI getURI() { + return uri; + } + + /** + * Returns the protocol version this channel uses.
For more infos see + * https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts + * + * @return The draft used for this client + */ + public Draft getDraft() { + return draft; + } + + /** + * Returns the socket to allow Hostname Verification + * + * @return the socket used for this connection + */ + public Socket getSocket() { + return socket; + } + + /** + * @param key Name of the header to add. + * @param value Value of the header to add. + * @since 1.4.1 Adds an additional header to be sent in the handshake.
If the connection is + * already made, adding headers has no effect, unless reconnect is called, which then a new + * handshake is sent.
If a header with the same key already exists, it is overridden. + */ + public void addHeader(String key, String value) { + if (headers == null) { + headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + headers.put(key, value); + } + + /** + * @param key Name of the header to remove. + * @return the previous value associated with key, or null if there was no mapping for key. + * @since 1.4.1 Removes a header from the handshake to be sent, if header key exists.
+ */ + public String removeHeader(String key) { + if (headers == null) { + return null; + } + return headers.remove(key); + } + + /** + * @since 1.4.1 Clears all previously put headers. + */ + public void clearHeaders() { + headers = null; + } + + /** + * Sets a custom DNS resolver. + * + * @param dnsResolver The DnsResolver to use. + * @since 1.4.1 + */ + public void setDnsResolver(DnsResolver dnsResolver) { + this.dnsResolver = dnsResolver; + } + + /** + * Reinitiates the websocket connection. This method does not block. + * + * @since 1.3.8 + */ + public void reconnect() { + reset(); + connect(); + } + + /** + * Same as reconnect but blocks until the websocket reconnected or failed to do + * so.
+ * + * @return Returns whether it succeeded or not. + * @throws InterruptedException Thrown when the threads get interrupted + * @since 1.3.8 + */ + public boolean reconnectBlocking() throws InterruptedException { + reset(); + return connectBlocking(); + } + + /** + * Same as reconnect but blocks with a timeout until the websocket connected or failed + * to do so.
+ * + * @param timeout The connect timeout + * @param timeUnit The timeout time unit + * @return Returns whether it succeeded or not. + * @throws InterruptedException Thrown when the threads get interrupted + * @since 1.6.1 + */ + public boolean reconnectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException { + reset(); + return connectBlocking(timeout, timeUnit); + } + + /** + * Reset everything relevant to allow a reconnect + * + * @since 1.3.8 + */ + private void reset() { + Thread current = Thread.currentThread(); + if (current == writeThread || current == connectReadThread) { + throw new IllegalStateException( + "You cannot initialize a reconnect out of the websocket thread. Use reconnect in another thread to ensure a successful cleanup."); + } + try { + // This socket null check ensures we can reconnect a socket that failed to connect. It's an uncommon edge case, but we want to make sure we support it + if (engine.getReadyState() == ReadyState.NOT_YET_CONNECTED && socket != null) { + // Closing the socket when we have not connected prevents the writeThread from hanging on a write indefinitely during connection teardown + socket.close(); + } + closeBlocking(); + + if (writeThread != null) { + this.writeThread.interrupt(); + this.writeThread.join(); + this.writeThread = null; + } + if (connectReadThread != null) { + this.connectReadThread.interrupt(); + this.connectReadThread.join(); + this.connectReadThread = null; + } + this.draft.reset(); + if (this.socket != null) { + this.socket.close(); + this.socket = null; + } + } catch (Exception e) { + onError(e); + engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage()); + return; + } + connectLatch = new CountDownLatch(1); + closeLatch = new CountDownLatch(1); + this.engine = new WebSocketImpl(this, this.draft); + } + + /** + * Initiates the websocket connection. This method does not block. + */ + public void connect() { + if (connectReadThread != null) { + throw new IllegalStateException("WebSocketClient objects are not reuseable"); + } + connectReadThread = new Thread(this); + connectReadThread.setDaemon(isDaemon()); + connectReadThread.setName("WebSocketConnectReadThread-" + connectReadThread.getId()); + connectReadThread.start(); + } + + /** + * Same as connect but blocks until the websocket connected or failed to do so.
+ * + * @return Returns whether it succeeded or not. + * @throws InterruptedException Thrown when the threads get interrupted + */ + public boolean connectBlocking() throws InterruptedException { + connect(); + connectLatch.await(); + return engine.isOpen(); + } + + /** + * Same as connect but blocks with a timeout until the websocket connected or failed + * to do so.
+ * + * @param timeout The connect timeout + * @param timeUnit The timeout time unit + * @return Returns whether it succeeded or not. + * @throws InterruptedException Thrown when the threads get interrupted + */ + public boolean connectBlocking(long timeout, TimeUnit timeUnit) throws InterruptedException { + connect(); + + boolean connected = connectLatch.await(timeout, timeUnit); + if (!connected) { + reset(); + } + + return connected && engine.isOpen(); + } + + /** + * Initiates the websocket close handshake. This method does not block
In oder to make sure + * the connection is closed use closeBlocking + */ + public void close() { + if (writeThread != null) { + engine.close(CloseFrame.NORMAL); + } + } + + /** + * Same as close but blocks until the websocket closed or failed to do so.
+ * + * @throws InterruptedException Thrown when the threads get interrupted + */ + public void closeBlocking() throws InterruptedException { + close(); + closeLatch.await(); + } + + /** + * Sends text to the connected websocket server. + * + * @param text The string which will be transmitted. + */ + public void send(String text) { + engine.send(text); + } + + /** + * Sends binary data to the connected webSocket server. + * + * @param data The byte-Array of data to send to the WebSocket server. + */ + public void send(byte[] data) { + engine.send(data); + } + + @Override + public T getAttachment() { + return engine.getAttachment(); + } + + @Override + public void setAttachment(T attachment) { + engine.setAttachment(attachment); + } + + @Override + protected Collection getConnections() { + return Collections.singletonList((WebSocket) engine); + } + + @Override + public void sendPing() { + engine.sendPing(); + } + + public void run() { + InputStream istream; + try { + boolean upgradeSocketToSSLSocket = prepareSocket(); + + socket.setTcpNoDelay(isTcpNoDelay()); + socket.setReuseAddress(isReuseAddr()); + int receiveBufferSize = getReceiveBufferSize(); + if (receiveBufferSize > 0) { + socket.setReceiveBufferSize(receiveBufferSize); + } + + if (!socket.isConnected()) { + InetSocketAddress addr = dnsResolver == null ? InetSocketAddress.createUnresolved(uri.getHost(), getPort()) : new InetSocketAddress(dnsResolver.resolve(uri), this.getPort()); + socket.connect(addr, connectTimeout); + } + + // if the socket is set by others we don't apply any TLS wrapper + if (upgradeSocketToSSLSocket && "wss".equals(uri.getScheme())) { + upgradeSocketToSSL(); + } + + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + SSLParameters sslParameters = sslSocket.getSSLParameters(); + onSetSSLParameters(sslParameters); + sslSocket.setSSLParameters(sslParameters); + } + + istream = socket.getInputStream(); + ostream = socket.getOutputStream(); + + sendHandshake(); + } catch (/*IOException | SecurityException | UnresolvedAddressException | InvalidHandshakeException | ClosedByInterruptException | SocketTimeoutException */Exception e) { + onWebsocketError(engine, e); + engine.closeConnection(CloseFrame.NEVER_CONNECTED, e.getMessage()); + return; + } catch (InternalError e) { + // https://bugs.openjdk.java.net/browse/JDK-8173620 + if (e.getCause() instanceof InvocationTargetException && e.getCause() + .getCause() instanceof IOException) { + IOException cause = (IOException) e.getCause().getCause(); + onWebsocketError(engine, cause); + engine.closeConnection(CloseFrame.NEVER_CONNECTED, cause.getMessage()); + return; + } + throw e; + } + + if (writeThread != null) { + writeThread.interrupt(); + try { + writeThread.join(); + } catch (InterruptedException e) { + /* ignore */ + } + } + writeThread = new Thread(new WebsocketWriteThread(this)); + writeThread.setDaemon(isDaemon()); + writeThread.start(); + + int receiveBufferSize = getReceiveBufferSize(); + byte[] rawbuffer = new byte[receiveBufferSize > 0 ? receiveBufferSize : DEFAULT_READ_BUFFER_SIZE]; + int readBytes; + + try { + while (!isClosing() && !isClosed() && (readBytes = istream.read(rawbuffer)) != -1) { + engine.decode(ByteBuffer.wrap(rawbuffer, 0, readBytes)); + } + engine.eot(); + } catch (IOException e) { + handleIOException(e); + } catch (RuntimeException e) { + // this catch case covers internal errors only and indicates a bug in this websocket implementation + onError(e); + engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage()); + } + } + + private void upgradeSocketToSSL() + throws NoSuchAlgorithmException, KeyManagementException, IOException { + SSLSocketFactory factory; + // Prioritise the provided socketfactory + // Helps when using web debuggers like Fiddler Classic + if (socketFactory instanceof SSLSocketFactory) { + factory = (SSLSocketFactory) socketFactory; + } else { + factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + socket = factory.createSocket(socket, uri.getHost(), getPort(), true); + } + + private boolean prepareSocket() throws IOException { + boolean upgradeSocketToSSLSocket = false; + // Prioritise a proxy over a socket factory and apply the socketfactory later + if (proxy != Proxy.NO_PROXY) { + socket = new Socket(proxy); + upgradeSocketToSSLSocket = true; + } else if (socketFactory != null) { + socket = socketFactory.createSocket(); + } else if (socket == null) { + socket = new Socket(proxy); + upgradeSocketToSSLSocket = true; + } else if (socket.isClosed()) { + throw new IOException(); + } + return upgradeSocketToSSLSocket; + } + + /** + * Apply specific SSLParameters If you override this method make sure to always call + * super.onSetSSLParameters() to ensure the hostname validation is active + * + * @param sslParameters the SSLParameters which will be used for the SSLSocket + */ + protected void onSetSSLParameters(SSLParameters sslParameters) { + // If you run into problem on Android (NoSuchMethodException), check out the wiki https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm + // Perform hostname validation + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + } + + /** + * Extract the specified port + * + * @return the specified port or the default port for the specific scheme + */ + private int getPort() { + int port = uri.getPort(); + String scheme = uri.getScheme(); + if ("wss".equals(scheme)) { + return port == -1 ? WebSocketImpl.DEFAULT_WSS_PORT : port; + } else if ("ws".equals(scheme)) { + return port == -1 ? WebSocketImpl.DEFAULT_PORT : port; + } else { + throw new IllegalArgumentException("unknown scheme: " + scheme); + } + } + + /** + * Create and send the handshake to the other endpoint + * + * @throws InvalidHandshakeException a invalid handshake was created + */ + private void sendHandshake() throws InvalidHandshakeException { + String path; + String part1 = uri.getRawPath(); + String part2 = uri.getRawQuery(); + if (part1 == null || part1.length() == 0) { + path = "/"; + } else { + path = part1; + } + if (part2 != null) { + path += '?' + part2; + } + int port = getPort(); + String host = uri.getHost() + ( + (port != WebSocketImpl.DEFAULT_PORT && port != WebSocketImpl.DEFAULT_WSS_PORT) + ? ":" + port + : ""); + + HandshakeImpl1Client handshake = new HandshakeImpl1Client(); + handshake.setResourceDescriptor(path); + handshake.put("Host", host); + if (headers != null) { + for (Map.Entry kv : headers.entrySet()) { + handshake.put(kv.getKey(), kv.getValue()); + } + } + engine.startHandshake(handshake); + } + + /** + * This represents the state of the connection. + */ + public ReadyState getReadyState() { + return engine.getReadyState(); + } + + /** + * Calls subclass' implementation of onMessage. + */ + @Override + public final void onWebsocketMessage(WebSocket conn, String message) { + onMessage(message); + } + + @Override + public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) { + onMessage(blob); + } + + /** + * Calls subclass' implementation of onOpen. + */ + @Override + public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) { + startConnectionLostTimer(); + onOpen((ServerHandshake) handshake); + connectLatch.countDown(); + } + + /** + * Calls subclass' implementation of onClose. + */ + @Override + public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) { + stopConnectionLostTimer(); + if (writeThread != null) { + writeThread.interrupt(); + } + onClose(code, reason, remote); + connectLatch.countDown(); + closeLatch.countDown(); + } + + /** + * Calls subclass' implementation of onIOError. + */ + @Override + public final void onWebsocketError(WebSocket conn, Exception ex) { + onError(ex); + } + + @Override + public final void onWriteDemand(WebSocket conn) { + // nothing to do + } + + @Override + public void onWebsocketCloseInitiated(WebSocket conn, int code, String reason) { + onCloseInitiated(code, reason); + } + + @Override + public void onWebsocketClosing(WebSocket conn, int code, String reason, boolean remote) { + onClosing(code, reason, remote); + } + + /** + * Send when this peer sends a close handshake + * + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + */ + public void onCloseInitiated(int code, String reason) { + //To overwrite + } + + /** + * Called as soon as no further frames are accepted + * + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + * @param remote Returns whether or not the closing of the connection was initiated by the remote + * host. + */ + public void onClosing(int code, String reason, boolean remote) { + //To overwrite + } + + /** + * Getter for the engine + * + * @return the engine + */ + public WebSocket getConnection() { + return engine; + } + + @Override + public InetSocketAddress getLocalSocketAddress(WebSocket conn) { + if (socket != null) { + return (InetSocketAddress) socket.getLocalSocketAddress(); + } + return null; + } + + @Override + public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { + if (socket != null) { + return (InetSocketAddress) socket.getRemoteSocketAddress(); + } + return null; + } + + // ABSTRACT METHODS ///////////////////////////////////////////////////////// + + /** + * Called after an opening handshake has been performed and the given websocket is ready to be + * written on. + * + * @param handshakedata The handshake of the websocket instance + */ + public abstract void onOpen(ServerHandshake handshakedata); + + /** + * Callback for string messages received from the remote host + * + * @param message The UTF-8 decoded message that was received. + * @see #onMessage(ByteBuffer) + **/ + public abstract void onMessage(String message); + + /** + * Called after the websocket connection has been closed. + * + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + * @param remote Returns whether or not the closing of the connection was initiated by the remote + * host. + **/ + public abstract void onClose(int code, String reason, boolean remote); + + /** + * Called when errors occurs. If an error causes the websocket connection to fail {@link + * #onClose(int, String, boolean)} will be called additionally.
This method will be called + * primarily because of IO or protocol errors.
If the given exception is an RuntimeException + * that probably means that you encountered a bug.
+ * + * @param ex The exception causing this error + **/ + public abstract void onError(Exception ex); + + /** + * Callback for binary messages received from the remote host + * + * @param bytes The binary message that was received. + * @see #onMessage(String) + **/ + public void onMessage(ByteBuffer bytes) { + //To overwrite + } + + + private class WebsocketWriteThread implements Runnable { + + private final WebSocketClient webSocketClient; + + WebsocketWriteThread(WebSocketClient webSocketClient) { + this.webSocketClient = webSocketClient; + } + + @Override + public void run() { + Thread.currentThread().setName("WebSocketWriteThread-" + Thread.currentThread().getId()); + try { + runWriteData(); + } catch (IOException e) { + handleIOException(e); + } finally { + closeSocket(); + } + } + + /** + * Write the data into the outstream + * + * @throws IOException if write or flush did not work + */ + private void runWriteData() throws IOException { + try { + while (!Thread.interrupted()) { + ByteBuffer buffer = engine.outQueue.take(); + ostream.write(buffer.array(), 0, buffer.limit()); + ostream.flush(); + } + } catch (InterruptedException e) { + for (ByteBuffer buffer : engine.outQueue) { + ostream.write(buffer.array(), 0, buffer.limit()); + ostream.flush(); + } + Thread.currentThread().interrupt(); + } + } + + /** + * Closing the socket + */ + private void closeSocket() { + try { + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + onWebsocketError(webSocketClient, ex); + } + } + } + + + /** + * Method to set a proxy for this connection + * + * @param proxy the proxy to use for this websocket client + */ + public void setProxy(Proxy proxy) { + if (proxy == null) { + throw new IllegalArgumentException(); + } + this.proxy = proxy; + } + + /** + * Accepts bound and unbound sockets.
This method must be called before connect. + * If the given socket is not yet bound it will be bound to the uri specified in the constructor. + * + * @param socket The socket which should be used for the connection + * @deprecated use setSocketFactory + */ + @Deprecated + public void setSocket(Socket socket) { + if (this.socket != null) { + throw new IllegalStateException("socket has already been set"); + } + this.socket = socket; + } + + /** + * Accepts a SocketFactory.
This method must be called before connect. The socket + * will be bound to the uri specified in the constructor. + * + * @param socketFactory The socket factory which should be used for the connection. + */ + public void setSocketFactory(SocketFactory socketFactory) { + this.socketFactory = socketFactory; + } + + @Override + public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) { + engine.sendFragmentedFrame(op, buffer, fin); + } + + @Override + public boolean isOpen() { + return engine.isOpen(); + } + + @Override + public boolean isFlushAndClose() { + return engine.isFlushAndClose(); + } + + @Override + public boolean isClosed() { + return engine.isClosed(); + } + + @Override + public boolean isClosing() { + return engine.isClosing(); + } + + @Override + public boolean hasBufferedData() { + return engine.hasBufferedData(); + } + + @Override + public void close(int code) { + engine.close(code); + } + + @Override + public void close(int code, String message) { + engine.close(code, message); + } + + @Override + public void closeConnection(int code, String message) { + engine.closeConnection(code, message); + } + + @Override + public void send(ByteBuffer bytes) { + engine.send(bytes); + } + + @Override + public void sendFrame(Framedata framedata) { + engine.sendFrame(framedata); + } + + @Override + public void sendFrame(Collection frames) { + engine.sendFrame(frames); + } + + @Override + public InetSocketAddress getLocalSocketAddress() { + return engine.getLocalSocketAddress(); + } + + @Override + public InetSocketAddress getRemoteSocketAddress() { + return engine.getRemoteSocketAddress(); + } + + @Override + public String getResourceDescriptor() { + return uri.getPath(); + } + + @Override + public boolean hasSSLSupport() { + return socket instanceof SSLSocket; + } + + @Override + public SSLSession getSSLSession() { + if (!hasSSLSupport()) { + throw new IllegalArgumentException( + "This websocket uses ws instead of wss. No SSLSession available."); + } + return ((SSLSocket)socket).getSession(); + } + + @Override + public IProtocol getProtocol() { + return engine.getProtocol(); + } + + /** + * Method to give some additional info for specific IOExceptions + * + * @param e the IOException causing a eot. + */ + private void handleIOException(IOException e) { + if (e instanceof SSLException) { + onError(e); + } + engine.eot(); + } +} diff --git a/src/main/java/org/java_websocket/client/package-info.java b/src/main/java/org/java_websocket/client/package-info.java new file mode 100644 index 0000000..e6d799d --- /dev/null +++ b/src/main/java/org/java_websocket/client/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all implementations in relation with the WebSocketClient. + */ +package org.java_websocket.client; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/drafts/Draft.java b/src/main/java/org/java_websocket/drafts/Draft.java new file mode 100644 index 0000000..2cda1e5 --- /dev/null +++ b/src/main/java/org/java_websocket/drafts/Draft.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.drafts; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.enums.CloseHandshakeType; +import org.java_websocket.enums.HandshakeState; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.Role; +import org.java_websocket.exceptions.IncompleteHandshakeException; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.framing.BinaryFrame; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.ContinuousFrame; +import org.java_websocket.framing.DataFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.TextFrame; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ClientHandshakeBuilder; +import org.java_websocket.handshake.HandshakeBuilder; +import org.java_websocket.handshake.HandshakeImpl1Client; +import org.java_websocket.handshake.HandshakeImpl1Server; +import org.java_websocket.handshake.Handshakedata; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; +import org.java_websocket.util.Charsetfunctions; + +/** + * Base class for everything of a websocket specification which is not common such as the way the + * handshake is read or frames are transferred. + **/ +public abstract class Draft { + + /** + * In some cases the handshake will be parsed different depending on whether + */ + protected Role role = null; + + protected Opcode continuousFrameType = null; + + public static ByteBuffer readLine(ByteBuffer buf) { + ByteBuffer sbuf = ByteBuffer.allocate(buf.remaining()); + byte prev; + byte cur = '0'; + while (buf.hasRemaining()) { + prev = cur; + cur = buf.get(); + sbuf.put(cur); + if (prev == (byte) '\r' && cur == (byte) '\n') { + sbuf.limit(sbuf.position() - 2); + sbuf.position(0); + return sbuf; + + } + } + // ensure that there wont be any bytes skipped + buf.position(buf.position() - sbuf.position()); + return null; + } + + public static String readStringLine(ByteBuffer buf) { + ByteBuffer b = readLine(buf); + return b == null ? null : Charsetfunctions.stringAscii(b.array(), 0, b.limit()); + } + + public static HandshakeBuilder translateHandshakeHttp(ByteBuffer buf, Role role) + throws InvalidHandshakeException { + HandshakeBuilder handshake; + + String line = readStringLine(buf); + if (line == null) { + throw new IncompleteHandshakeException(buf.capacity() + 128); + } + + String[] firstLineTokens = line.split(" ", 3);// eg. HTTP/1.1 101 Switching the Protocols + if (firstLineTokens.length != 3) { + throw new InvalidHandshakeException(); + } + if (role == Role.CLIENT) { + handshake = translateHandshakeHttpClient(firstLineTokens, line); + } else { + handshake = translateHandshakeHttpServer(firstLineTokens, line); + } + line = readStringLine(buf); + while (line != null && line.length() > 0) { + String[] pair = line.split(":", 2); + if (pair.length != 2) { + throw new InvalidHandshakeException("not an http header"); + } + // If the handshake contains already a specific key, append the new value + if (handshake.hasFieldValue(pair[0])) { + handshake.put(pair[0], + handshake.getFieldValue(pair[0]) + "; " + pair[1].replaceFirst("^ +", "")); + } else { + handshake.put(pair[0], pair[1].replaceFirst("^ +", "")); + } + line = readStringLine(buf); + } + if (line == null) { + throw new IncompleteHandshakeException(); + } + return handshake; + } + + /** + * Checking the handshake for the role as server + * + * @param firstLineTokens the token of the first line split as as an string array + * @param line the whole line + * @return a handshake + */ + private static HandshakeBuilder translateHandshakeHttpServer(String[] firstLineTokens, + String line) throws InvalidHandshakeException { + // translating/parsing the request from the CLIENT + if (!"GET".equalsIgnoreCase(firstLineTokens[0])) { + throw new InvalidHandshakeException(String + .format("Invalid request method received: %s Status line: %s", firstLineTokens[0], line)); + } + if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[2])) { + throw new InvalidHandshakeException(String + .format("Invalid status line received: %s Status line: %s", firstLineTokens[2], line)); + } + ClientHandshakeBuilder clienthandshake = new HandshakeImpl1Client(); + clienthandshake.setResourceDescriptor(firstLineTokens[1]); + return clienthandshake; + } + + /** + * Checking the handshake for the role as client + * + * @param firstLineTokens the token of the first line split as as an string array + * @param line the whole line + * @return a handshake + */ + private static HandshakeBuilder translateHandshakeHttpClient(String[] firstLineTokens, + String line) throws InvalidHandshakeException { + // translating/parsing the response from the SERVER + if (!"101".equals(firstLineTokens[1])) { + throw new InvalidHandshakeException(String + .format("Invalid status code received: %s Status line: %s", firstLineTokens[1], line)); + } + if (!"HTTP/1.1".equalsIgnoreCase(firstLineTokens[0])) { + throw new InvalidHandshakeException(String + .format("Invalid status line received: %s Status line: %s", firstLineTokens[0], line)); + } + HandshakeBuilder handshake = new HandshakeImpl1Server(); + ServerHandshakeBuilder serverhandshake = (ServerHandshakeBuilder) handshake; + serverhandshake.setHttpStatus(Short.parseShort(firstLineTokens[1])); + serverhandshake.setHttpStatusMessage(firstLineTokens[2]); + return handshake; + } + + public abstract HandshakeState acceptHandshakeAsClient(ClientHandshake request, + ServerHandshake response) throws InvalidHandshakeException; + + public abstract HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) + throws InvalidHandshakeException; + + protected boolean basicAccept(Handshakedata handshakedata) { + return handshakedata.getFieldValue("Upgrade").equalsIgnoreCase("websocket") && handshakedata + .getFieldValue("Connection").toLowerCase(Locale.ENGLISH).contains("upgrade"); + } + + public abstract ByteBuffer createBinaryFrame(Framedata framedata); + + public abstract List createFrames(ByteBuffer binary, boolean mask); + + public abstract List createFrames(String text, boolean mask); + + + /** + * Handle the frame specific to the draft + * + * @param webSocketImpl the websocketimpl used for this draft + * @param frame the frame which is supposed to be handled + * @throws InvalidDataException will be thrown on invalid data + */ + public abstract void processFrame(WebSocketImpl webSocketImpl, Framedata frame) + throws InvalidDataException; + + public List continuousFrame(Opcode op, ByteBuffer buffer, boolean fin) { + if (op != Opcode.BINARY && op != Opcode.TEXT) { + throw new IllegalArgumentException("Only Opcode.BINARY or Opcode.TEXT are allowed"); + } + DataFrame bui = null; + if (continuousFrameType != null) { + bui = new ContinuousFrame(); + } else { + continuousFrameType = op; + if (op == Opcode.BINARY) { + bui = new BinaryFrame(); + } else if (op == Opcode.TEXT) { + bui = new TextFrame(); + } + } + bui.setPayload(buffer); + bui.setFin(fin); + try { + bui.isValid(); + } catch (InvalidDataException e) { + throw new IllegalArgumentException( + e); // can only happen when one builds close frames(Opcode.Close) + } + if (fin) { + continuousFrameType = null; + } else { + continuousFrameType = op; + } + return Collections.singletonList((Framedata) bui); + } + + public abstract void reset(); + + /** + * @deprecated use createHandshake without the role + */ + @Deprecated + public List createHandshake(Handshakedata handshakedata, Role ownrole) { + return createHandshake(handshakedata); + } + + public List createHandshake(Handshakedata handshakedata) { + return createHandshake(handshakedata, true); + } + + /** + * @deprecated use createHandshake without the role since it does not have any effect + */ + @Deprecated + public List createHandshake(Handshakedata handshakedata, Role ownrole, + boolean withcontent) { + return createHandshake(handshakedata, withcontent); + } + + public List createHandshake(Handshakedata handshakedata, boolean withcontent) { + StringBuilder bui = new StringBuilder(100); + if (handshakedata instanceof ClientHandshake) { + bui.append("GET ").append(((ClientHandshake) handshakedata).getResourceDescriptor()) + .append(" HTTP/1.1"); + } else if (handshakedata instanceof ServerHandshake) { + bui.append("HTTP/1.1 101 ").append(((ServerHandshake) handshakedata).getHttpStatusMessage()); + } else { + throw new IllegalArgumentException("unknown role"); + } + bui.append("\r\n"); + Iterator it = handshakedata.iterateHttpFields(); + while (it.hasNext()) { + String fieldname = it.next(); + String fieldvalue = handshakedata.getFieldValue(fieldname); + bui.append(fieldname); + bui.append(": "); + bui.append(fieldvalue); + bui.append("\r\n"); + } + bui.append("\r\n"); + byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString()); + + byte[] content = withcontent ? handshakedata.getContent() : null; + ByteBuffer bytebuffer = ByteBuffer + .allocate((content == null ? 0 : content.length) + httpheader.length); + bytebuffer.put(httpheader); + if (content != null) { + bytebuffer.put(content); + } + bytebuffer.flip(); + return Collections.singletonList(bytebuffer); + } + + public abstract ClientHandshakeBuilder postProcessHandshakeRequestAsClient( + ClientHandshakeBuilder request) throws InvalidHandshakeException; + + public abstract HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, + ServerHandshakeBuilder response) throws InvalidHandshakeException; + + public abstract List translateFrame(ByteBuffer buffer) throws InvalidDataException; + + public abstract CloseHandshakeType getCloseHandshakeType(); + + /** + * Drafts must only be by one websocket at all. To prevent drafts to be used more than once the + * Websocket implementation should call this method in order to create a new usable version of a + * given draft instance.
The copy can be safely used in conjunction with a new websocket + * connection. + * + * @return a copy of the draft + */ + public abstract Draft copyInstance(); + + public Handshakedata translateHandshake(ByteBuffer buf) throws InvalidHandshakeException { + return translateHandshakeHttp(buf, role); + } + + public int checkAlloc(int bytecount) throws InvalidDataException { + if (bytecount < 0) { + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Negative count"); + } + return bytecount; + } + + int readVersion(Handshakedata handshakedata) { + String vers = handshakedata.getFieldValue("Sec-WebSocket-Version"); + if (vers.length() > 0) { + int v; + try { + v = Integer.parseInt(vers.trim()); + return v; + } catch (NumberFormatException e) { + return -1; + } + } + return -1; + } + + public void setParseMode(Role role) { + this.role = role; + } + + public Role getRole() { + return role; + } + + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/org/java_websocket/drafts/Draft_6455.java b/src/main/java/org/java_websocket/drafts/Draft_6455.java new file mode 100644 index 0000000..eb48799 --- /dev/null +++ b/src/main/java/org/java_websocket/drafts/Draft_6455.java @@ -0,0 +1,1213 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.drafts; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.enums.CloseHandshakeType; +import org.java_websocket.enums.HandshakeState; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.ReadyState; +import org.java_websocket.enums.Role; +import org.java_websocket.exceptions.IncompleteException; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.exceptions.LimitExceededException; +import org.java_websocket.exceptions.NotSendableException; +import org.java_websocket.extensions.DefaultExtension; +import org.java_websocket.extensions.IExtension; +import org.java_websocket.framing.BinaryFrame; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.FramedataImpl1; +import org.java_websocket.framing.TextFrame; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ClientHandshakeBuilder; +import org.java_websocket.handshake.HandshakeBuilder; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; +import org.java_websocket.protocols.IProtocol; +import org.java_websocket.protocols.Protocol; +import org.java_websocket.util.Base64; +import org.java_websocket.util.Charsetfunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation for the RFC 6455 websocket protocol This is the recommended class for your + * websocket connection + */ +public class Draft_6455 extends Draft { + + /** + * Handshake specific field for the key + */ + private static final String SEC_WEB_SOCKET_KEY = "Sec-WebSocket-Key"; + + /** + * Handshake specific field for the protocol + */ + private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + /** + * Handshake specific field for the extension + */ + private static final String SEC_WEB_SOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; + + /** + * Handshake specific field for the accept + */ + private static final String SEC_WEB_SOCKET_ACCEPT = "Sec-WebSocket-Accept"; + + /** + * Handshake specific field for the upgrade + */ + private static final String UPGRADE = "Upgrade"; + + /** + * Handshake specific field for the connection + */ + private static final String CONNECTION = "Connection"; + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(Draft_6455.class); + + /** + * Attribute for the used extension in this draft + */ + private IExtension negotiatedExtension = new DefaultExtension(); + + /** + * Attribute for the default extension + */ + private IExtension defaultExtension = new DefaultExtension(); + + /** + * Attribute for all available extension in this draft + */ + private List knownExtensions; + + /** + * Current active extension used to decode messages + */ + private IExtension currentDecodingExtension; + + /** + * Attribute for the used protocol in this draft + */ + private IProtocol protocol; + + /** + * Attribute for all available protocols in this draft + */ + private List knownProtocols; + + /** + * Attribute for the current continuous frame + */ + private Framedata currentContinuousFrame; + + /** + * Attribute for the payload of the current continuous frame + */ + private final List byteBufferList; + + /** + * Attribute for the current incomplete frame + */ + private ByteBuffer incompleteframe; + + /** + * Attribute for the reusable random instance + */ + private final SecureRandom reuseableRandom = new SecureRandom(); + + /** + * Attribute for the maximum allowed size of a frame + * + * @since 1.4.0 + */ + private int maxFrameSize; + + /** + * Constructor for the websocket protocol specified by RFC 6455 with default extensions + * + * @since 1.3.5 + */ + public Draft_6455() { + this(Collections.emptyList()); + } + + /** + * Constructor for the websocket protocol specified by RFC 6455 with custom extensions + * + * @param inputExtension the extension which should be used for this draft + * @since 1.3.5 + */ + public Draft_6455(IExtension inputExtension) { + this(Collections.singletonList(inputExtension)); + } + + /** + * Constructor for the websocket protocol specified by RFC 6455 with custom extensions + * + * @param inputExtensions the extensions which should be used for this draft + * @since 1.3.5 + */ + public Draft_6455(List inputExtensions) { + this(inputExtensions, Collections.singletonList(new Protocol(""))); + } + + /** + * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and + * protocols + * + * @param inputExtensions the extensions which should be used for this draft + * @param inputProtocols the protocols which should be used for this draft + * @since 1.3.7 + */ + public Draft_6455(List inputExtensions, List inputProtocols) { + this(inputExtensions, inputProtocols, Integer.MAX_VALUE); + } + + /** + * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and + * protocols + * + * @param inputExtensions the extensions which should be used for this draft + * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded + * frames can be bigger) + * @since 1.4.0 + */ + public Draft_6455(List inputExtensions, int inputMaxFrameSize) { + this(inputExtensions, Collections.singletonList(new Protocol("")), + inputMaxFrameSize); + } + + /** + * Constructor for the websocket protocol specified by RFC 6455 with custom extensions and + * protocols + * + * @param inputExtensions the extensions which should be used for this draft + * @param inputProtocols the protocols which should be used for this draft + * @param inputMaxFrameSize the maximum allowed size of a frame (the real payload size, decoded + * frames can be bigger) + * @since 1.4.0 + */ + public Draft_6455(List inputExtensions, List inputProtocols, + int inputMaxFrameSize) { + if (inputExtensions == null || inputProtocols == null || inputMaxFrameSize < 1) { + throw new IllegalArgumentException(); + } + knownExtensions = new ArrayList<>(inputExtensions.size()); + knownProtocols = new ArrayList<>(inputProtocols.size()); + boolean hasDefault = false; + byteBufferList = new ArrayList<>(); + for (IExtension inputExtension : inputExtensions) { + if (inputExtension.getClass().equals(DefaultExtension.class)) { + hasDefault = true; + } + } + knownExtensions.addAll(inputExtensions); + //We always add the DefaultExtension to implement the normal RFC 6455 specification + if (!hasDefault) { + knownExtensions.add(this.knownExtensions.size(), negotiatedExtension); + } + knownProtocols.addAll(inputProtocols); + maxFrameSize = inputMaxFrameSize; + currentDecodingExtension = null; + } + + @Override + public HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) + throws InvalidHandshakeException { + int v = readVersion(handshakedata); + if (v != 13) { + log.trace("acceptHandshakeAsServer - Wrong websocket version."); + return HandshakeState.NOT_MATCHED; + } + HandshakeState extensionState = HandshakeState.NOT_MATCHED; + String requestedExtension = handshakedata.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS); + for (IExtension knownExtension : knownExtensions) { + if (knownExtension.acceptProvidedExtensionAsServer(requestedExtension)) { + negotiatedExtension = knownExtension; + extensionState = HandshakeState.MATCHED; + log.trace("acceptHandshakeAsServer - Matching extension found: {}", negotiatedExtension); + break; + } + } + HandshakeState protocolState = containsRequestedProtocol( + handshakedata.getFieldValue(SEC_WEB_SOCKET_PROTOCOL)); + if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) { + return HandshakeState.MATCHED; + } + log.trace("acceptHandshakeAsServer - No matching extension or protocol found."); + return HandshakeState.NOT_MATCHED; + } + + /** + * Check if the requested protocol is part of this draft + * + * @param requestedProtocol the requested protocol + * @return MATCHED if it is matched, otherwise NOT_MATCHED + */ + private HandshakeState containsRequestedProtocol(String requestedProtocol) { + for (IProtocol knownProtocol : knownProtocols) { + if (knownProtocol.acceptProvidedProtocol(requestedProtocol)) { + protocol = knownProtocol; + log.trace("acceptHandshake - Matching protocol found: {}", protocol); + return HandshakeState.MATCHED; + } + } + return HandshakeState.NOT_MATCHED; + } + + @Override + public HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) + throws InvalidHandshakeException { + if (!basicAccept(response)) { + log.trace("acceptHandshakeAsClient - Missing/wrong upgrade or connection in handshake."); + return HandshakeState.NOT_MATCHED; + } + if (!request.hasFieldValue(SEC_WEB_SOCKET_KEY) || !response + .hasFieldValue(SEC_WEB_SOCKET_ACCEPT)) { + log.trace("acceptHandshakeAsClient - Missing Sec-WebSocket-Key or Sec-WebSocket-Accept"); + return HandshakeState.NOT_MATCHED; + } + + String seckeyAnswer = response.getFieldValue(SEC_WEB_SOCKET_ACCEPT); + String seckeyChallenge = request.getFieldValue(SEC_WEB_SOCKET_KEY); + seckeyChallenge = generateFinalKey(seckeyChallenge); + + if (!seckeyChallenge.equals(seckeyAnswer)) { + log.trace("acceptHandshakeAsClient - Wrong key for Sec-WebSocket-Key."); + return HandshakeState.NOT_MATCHED; + } + HandshakeState extensionState = HandshakeState.NOT_MATCHED; + String requestedExtension = response.getFieldValue(SEC_WEB_SOCKET_EXTENSIONS); + for (IExtension knownExtension : knownExtensions) { + if (knownExtension.acceptProvidedExtensionAsClient(requestedExtension)) { + negotiatedExtension = knownExtension; + extensionState = HandshakeState.MATCHED; + log.trace("acceptHandshakeAsClient - Matching extension found: {}", negotiatedExtension); + break; + } + } + HandshakeState protocolState = containsRequestedProtocol( + response.getFieldValue(SEC_WEB_SOCKET_PROTOCOL)); + if (protocolState == HandshakeState.MATCHED && extensionState == HandshakeState.MATCHED) { + return HandshakeState.MATCHED; + } + log.trace("acceptHandshakeAsClient - No matching extension or protocol found."); + return HandshakeState.NOT_MATCHED; + } + + /** + * Getter for the extension which is used by this draft + * + * @return the extension which is used or null, if handshake is not yet done + */ + public IExtension getExtension() { + return negotiatedExtension; + } + + /** + * Getter for all available extensions for this draft + * + * @return the extensions which are enabled for this draft + */ + public List getKnownExtensions() { + return knownExtensions; + } + + /** + * Getter for the protocol which is used by this draft + * + * @return the protocol which is used or null, if handshake is not yet done or no valid protocols + * @since 1.3.7 + */ + public IProtocol getProtocol() { + return protocol; + } + + + /** + * Getter for the maximum allowed payload size which is used by this draft + * + * @return the size, which is allowed for the payload + * @since 1.4.0 + */ + public int getMaxFrameSize() { + return maxFrameSize; + } + + /** + * Getter for all available protocols for this draft + * + * @return the protocols which are enabled for this draft + * @since 1.3.7 + */ + public List getKnownProtocols() { + return knownProtocols; + } + + @Override + public ClientHandshakeBuilder postProcessHandshakeRequestAsClient( + ClientHandshakeBuilder request) { + request.put(UPGRADE, "websocket"); + request.put(CONNECTION, UPGRADE); // to respond to a Connection keep alives + byte[] random = new byte[16]; + reuseableRandom.nextBytes(random); + request.put(SEC_WEB_SOCKET_KEY, Base64.encodeBytes(random)); + request.put("Sec-WebSocket-Version", "13");// overwriting the previous + StringBuilder requestedExtensions = new StringBuilder(); + for (IExtension knownExtension : knownExtensions) { + if (knownExtension.getProvidedExtensionAsClient() != null + && knownExtension.getProvidedExtensionAsClient().length() != 0) { + if (requestedExtensions.length() > 0) { + requestedExtensions.append(", "); + } + requestedExtensions.append(knownExtension.getProvidedExtensionAsClient()); + } + } + if (requestedExtensions.length() != 0) { + request.put(SEC_WEB_SOCKET_EXTENSIONS, requestedExtensions.toString()); + } + StringBuilder requestedProtocols = new StringBuilder(); + for (IProtocol knownProtocol : knownProtocols) { + if (knownProtocol.getProvidedProtocol().length() != 0) { + if (requestedProtocols.length() > 0) { + requestedProtocols.append(", "); + } + requestedProtocols.append(knownProtocol.getProvidedProtocol()); + } + } + if (requestedProtocols.length() != 0) { + request.put(SEC_WEB_SOCKET_PROTOCOL, requestedProtocols.toString()); + } + return request; + } + + @Override + public HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, + ServerHandshakeBuilder response) throws InvalidHandshakeException { + response.put(UPGRADE, "websocket"); + response.put(CONNECTION, + request.getFieldValue(CONNECTION)); // to respond to a Connection keep alives + String seckey = request.getFieldValue(SEC_WEB_SOCKET_KEY); + if (seckey == null || "".equals(seckey)) { + throw new InvalidHandshakeException("missing Sec-WebSocket-Key"); + } + response.put(SEC_WEB_SOCKET_ACCEPT, generateFinalKey(seckey)); + if (getExtension().getProvidedExtensionAsServer().length() != 0) { + response.put(SEC_WEB_SOCKET_EXTENSIONS, getExtension().getProvidedExtensionAsServer()); + } + if (getProtocol() != null && getProtocol().getProvidedProtocol().length() != 0) { + response.put(SEC_WEB_SOCKET_PROTOCOL, getProtocol().getProvidedProtocol()); + } + response.setHttpStatusMessage("Web Socket Protocol Handshake"); + response.put("Server", "TooTallNate Java-WebSocket"); + response.put("Date", getServerTime()); + return response; + } + + @Override + public Draft copyInstance() { + ArrayList newExtensions = new ArrayList<>(); + for (IExtension knownExtension : getKnownExtensions()) { + newExtensions.add(knownExtension.copyInstance()); + } + ArrayList newProtocols = new ArrayList<>(); + for (IProtocol knownProtocol : getKnownProtocols()) { + newProtocols.add(knownProtocol.copyInstance()); + } + return new Draft_6455(newExtensions, newProtocols, maxFrameSize); + } + + @Override + public ByteBuffer createBinaryFrame(Framedata framedata) { + getExtension().encodeFrame(framedata); + if (log.isTraceEnabled()) { + log.trace("afterEnconding({}): {}", framedata.getPayloadData().remaining(), + (framedata.getPayloadData().remaining() > 1000 ? "too big to display" + : new String(framedata.getPayloadData().array()))); + } + return createByteBufferFromFramedata(framedata); + } + + private ByteBuffer createByteBufferFromFramedata(Framedata framedata) { + ByteBuffer mes = framedata.getPayloadData(); + boolean mask = role == Role.CLIENT; + int sizebytes = getSizeBytes(mes); + ByteBuffer buf = ByteBuffer.allocate( + 1 + (sizebytes > 1 ? sizebytes + 1 : sizebytes) + (mask ? 4 : 0) + mes.remaining()); + byte optcode = fromOpcode(framedata.getOpcode()); + byte one = (byte) (framedata.isFin() ? -128 : 0); + one |= optcode; + if (framedata.isRSV1()) { + one |= getRSVByte(1); + } + if (framedata.isRSV2()) { + one |= getRSVByte(2); + } + if (framedata.isRSV3()) { + one |= getRSVByte(3); + } + buf.put(one); + byte[] payloadlengthbytes = toByteArray(mes.remaining(), sizebytes); + assert (payloadlengthbytes.length == sizebytes); + + if (sizebytes == 1) { + buf.put((byte) (payloadlengthbytes[0] | getMaskByte(mask))); + } else if (sizebytes == 2) { + buf.put((byte) ((byte) 126 | getMaskByte(mask))); + buf.put(payloadlengthbytes); + } else if (sizebytes == 8) { + buf.put((byte) ((byte) 127 | getMaskByte(mask))); + buf.put(payloadlengthbytes); + } else { + throw new IllegalStateException("Size representation not supported/specified"); + } + if (mask) { + ByteBuffer maskkey = ByteBuffer.allocate(4); + maskkey.putInt(reuseableRandom.nextInt()); + buf.put(maskkey.array()); + for (int i = 0; mes.hasRemaining(); i++) { + buf.put((byte) (mes.get() ^ maskkey.get(i % 4))); + } + } else { + buf.put(mes); + //Reset the position of the bytebuffer e.g. for additional use + mes.flip(); + } + assert (buf.remaining() == 0) : buf.remaining(); + buf.flip(); + return buf; + } + + private Framedata translateSingleFrame(ByteBuffer buffer) + throws IncompleteException, InvalidDataException { + if (buffer == null) { + throw new IllegalArgumentException(); + } + int maxpacketsize = buffer.remaining(); + int realpacketsize = 2; + translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); + byte b1 = buffer.get(/*0*/); + boolean fin = b1 >> 8 != 0; + boolean rsv1 = (b1 & 0x40) != 0; + boolean rsv2 = (b1 & 0x20) != 0; + boolean rsv3 = (b1 & 0x10) != 0; + byte b2 = buffer.get(/*1*/); + boolean mask = (b2 & -128) != 0; + int payloadlength = (byte) (b2 & ~(byte) 128); + Opcode optcode = toOpcode((byte) (b1 & 15)); + + if (!(payloadlength >= 0 && payloadlength <= 125)) { + TranslatedPayloadMetaData payloadData = translateSingleFramePayloadLength(buffer, optcode, + payloadlength, maxpacketsize, realpacketsize); + payloadlength = payloadData.getPayloadLength(); + realpacketsize = payloadData.getRealPackageSize(); + } + translateSingleFrameCheckLengthLimit(payloadlength); + realpacketsize += (mask ? 4 : 0); + realpacketsize += payloadlength; + translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); + + ByteBuffer payload = ByteBuffer.allocate(checkAlloc(payloadlength)); + if (mask) { + byte[] maskskey = new byte[4]; + buffer.get(maskskey); + for (int i = 0; i < payloadlength; i++) { + payload.put((byte) (buffer.get(/*payloadstart + i*/) ^ maskskey[i % 4])); + } + } else { + payload.put(buffer.array(), buffer.position(), payload.limit()); + buffer.position(buffer.position() + payload.limit()); + } + + FramedataImpl1 frame = FramedataImpl1.get(optcode); + frame.setFin(fin); + frame.setRSV1(rsv1); + frame.setRSV2(rsv2); + frame.setRSV3(rsv3); + payload.flip(); + frame.setPayload(payload); + if (frame.getOpcode() != Opcode.CONTINUOUS) { + // Prioritize the negotiated extension + if (frame.isRSV1() || frame.isRSV2() || frame.isRSV3()) { + currentDecodingExtension = getExtension(); + } else { + // No encoded message, so we can use the default one + currentDecodingExtension = defaultExtension; + } + } + if (currentDecodingExtension == null) { + currentDecodingExtension = defaultExtension; + } + currentDecodingExtension.isFrameValid(frame); + currentDecodingExtension.decodeFrame(frame); + if (log.isTraceEnabled()) { + log.trace("afterDecoding({}): {}", frame.getPayloadData().remaining(), + (frame.getPayloadData().remaining() > 1000 ? "too big to display" + : new String(frame.getPayloadData().array()))); + } + frame.isValid(); + return frame; + } + + /** + * Translate the buffer depending when it has an extended payload length (126 or 127) + * + * @param buffer the buffer to read from + * @param optcode the decoded optcode + * @param oldPayloadlength the old payload length + * @param maxpacketsize the max packet size allowed + * @param oldRealpacketsize the real packet size + * @return the new payload data containing new payload length and new packet size + * @throws InvalidFrameException thrown if a control frame has an invalid length + * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize + * @throws LimitExceededException if the payload length is to big + */ + private TranslatedPayloadMetaData translateSingleFramePayloadLength(ByteBuffer buffer, + Opcode optcode, int oldPayloadlength, int maxpacketsize, int oldRealpacketsize) + throws InvalidFrameException, IncompleteException, LimitExceededException { + int payloadlength = oldPayloadlength; + int realpacketsize = oldRealpacketsize; + if (optcode == Opcode.PING || optcode == Opcode.PONG || optcode == Opcode.CLOSING) { + log.trace("Invalid frame: more than 125 octets"); + throw new InvalidFrameException("more than 125 octets"); + } + if (payloadlength == 126) { + realpacketsize += 2; // additional length bytes + translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); + byte[] sizebytes = new byte[3]; + sizebytes[1] = buffer.get(/*1 + 1*/); + sizebytes[2] = buffer.get(/*1 + 2*/); + payloadlength = new BigInteger(sizebytes).intValue(); + } else { + realpacketsize += 8; // additional length bytes + translateSingleFrameCheckPacketSize(maxpacketsize, realpacketsize); + byte[] bytes = new byte[8]; + for (int i = 0; i < 8; i++) { + bytes[i] = buffer.get(/*1 + i*/); + } + long length = new BigInteger(bytes).longValue(); + translateSingleFrameCheckLengthLimit(length); + payloadlength = (int) length; + } + return new TranslatedPayloadMetaData(payloadlength, realpacketsize); + } + + /** + * Check if the frame size exceeds the allowed limit + * + * @param length the current payload length + * @throws LimitExceededException if the payload length is to big + */ + private void translateSingleFrameCheckLengthLimit(long length) throws LimitExceededException { + if (length > Integer.MAX_VALUE) { + log.trace("Limit exedeed: Payloadsize is to big..."); + throw new LimitExceededException("Payloadsize is to big..."); + } + if (length > maxFrameSize) { + log.trace("Payload limit reached. Allowed: {} Current: {}", maxFrameSize, length); + throw new LimitExceededException("Payload limit reached.", maxFrameSize); + } + if (length < 0) { + log.trace("Limit underflow: Payloadsize is to little..."); + throw new LimitExceededException("Payloadsize is to little..."); + } + } + + /** + * Check if the max packet size is smaller than the real packet size + * + * @param maxpacketsize the max packet size + * @param realpacketsize the real packet size + * @throws IncompleteException if the maxpacketsize is smaller than the realpackagesize + */ + private void translateSingleFrameCheckPacketSize(int maxpacketsize, int realpacketsize) + throws IncompleteException { + if (maxpacketsize < realpacketsize) { + log.trace("Incomplete frame: maxpacketsize < realpacketsize"); + throw new IncompleteException(realpacketsize); + } + } + + /** + * Get a byte that can set RSV bits when OR(|)'d. 0 1 2 3 4 5 6 7 +-+-+-+-+-------+ |F|R|R|R| + * opcode| |I|S|S|S| (4) | |N|V|V|V| | | |1|2|3| | + * + * @param rsv Can only be {0, 1, 2, 3} + * @return byte that represents which RSV bit is set. + */ + private byte getRSVByte(int rsv) { + switch (rsv) { + case 1 : // 0100 0000 + return 0x40; + case 2 : // 0010 0000 + return 0x20; + case 3 : // 0001 0000 + return 0x10; + default: + return 0; + } + } + + /** + * Get the mask byte if existing + * + * @param mask is mask active or not + * @return -128 for true, 0 for false + */ + private byte getMaskByte(boolean mask) { + return mask ? (byte) -128 : 0; + } + + /** + * Get the size bytes for the byte buffer + * + * @param mes the current buffer + * @return the size bytes + */ + private int getSizeBytes(ByteBuffer mes) { + if (mes.remaining() <= 125) { + return 1; + } else if (mes.remaining() <= 65535) { + return 2; + } + return 8; + } + + @Override + public List translateFrame(ByteBuffer buffer) throws InvalidDataException { + while (true) { + List frames = new LinkedList<>(); + Framedata cur; + if (incompleteframe != null) { + // complete an incomplete frame + try { + buffer.mark(); + int availableNextByteCount = buffer.remaining();// The number of bytes received + int expectedNextByteCount = incompleteframe + .remaining();// The number of bytes to complete the incomplete frame + + if (expectedNextByteCount > availableNextByteCount) { + // did not receive enough bytes to complete the frame + incompleteframe.put(buffer.array(), buffer.position(), availableNextByteCount); + buffer.position(buffer.position() + availableNextByteCount); + return Collections.emptyList(); + } + incompleteframe.put(buffer.array(), buffer.position(), expectedNextByteCount); + buffer.position(buffer.position() + expectedNextByteCount); + cur = translateSingleFrame((ByteBuffer) incompleteframe.duplicate().position(0)); + frames.add(cur); + incompleteframe = null; + } catch (IncompleteException e) { + // extending as much as suggested + ByteBuffer extendedframe = ByteBuffer.allocate(checkAlloc(e.getPreferredSize())); + assert (extendedframe.limit() > incompleteframe.limit()); + incompleteframe.rewind(); + extendedframe.put(incompleteframe); + incompleteframe = extendedframe; + continue; + } + } + + // Read as much as possible full frames + while (buffer.hasRemaining()) { + buffer.mark(); + try { + cur = translateSingleFrame(buffer); + frames.add(cur); + } catch (IncompleteException e) { + // remember the incomplete data + buffer.reset(); + int pref = e.getPreferredSize(); + incompleteframe = ByteBuffer.allocate(checkAlloc(pref)); + incompleteframe.put(buffer); + break; + } + } + return frames; + } + } + + @Override + public List createFrames(ByteBuffer binary, boolean mask) { + BinaryFrame curframe = new BinaryFrame(); + curframe.setPayload(binary); + curframe.setTransferemasked(mask); + try { + curframe.isValid(); + } catch (InvalidDataException e) { + throw new NotSendableException(e); + } + return Collections.singletonList((Framedata) curframe); + } + + @Override + public List createFrames(String text, boolean mask) { + TextFrame curframe = new TextFrame(); + curframe.setPayload(ByteBuffer.wrap(Charsetfunctions.utf8Bytes(text))); + curframe.setTransferemasked(mask); + try { + curframe.isValid(); + } catch (InvalidDataException e) { + throw new NotSendableException(e); + } + return Collections.singletonList((Framedata) curframe); + } + + @Override + public void reset() { + incompleteframe = null; + if (negotiatedExtension != null) { + negotiatedExtension.reset(); + } + negotiatedExtension = new DefaultExtension(); + protocol = null; + } + + /** + * Generate a date for for the date-header + * + * @return the server time + */ + private String getServerTime() { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat.format(calendar.getTime()); + } + + /** + * Generate a final key from a input string + * + * @param in the input string + * @return a final key + */ + private String generateFinalKey(String in) { + String seckey = in.trim(); + String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + MessageDigest sh1; + try { + sh1 = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + return Base64.encodeBytes(sh1.digest(acc.getBytes())); + } + + private byte[] toByteArray(long val, int bytecount) { + byte[] buffer = new byte[bytecount]; + int highest = 8 * bytecount - 8; + for (int i = 0; i < bytecount; i++) { + buffer[i] = (byte) (val >>> (highest - 8 * i)); + } + return buffer; + } + + + private byte fromOpcode(Opcode opcode) { + if (opcode == Opcode.CONTINUOUS) { + return 0; + } else if (opcode == Opcode.TEXT) { + return 1; + } else if (opcode == Opcode.BINARY) { + return 2; + } else if (opcode == Opcode.CLOSING) { + return 8; + } else if (opcode == Opcode.PING) { + return 9; + } else if (opcode == Opcode.PONG) { + return 10; + } + throw new IllegalArgumentException("Don't know how to handle " + opcode.toString()); + } + + private Opcode toOpcode(byte opcode) throws InvalidFrameException { + switch (opcode) { + case 0: + return Opcode.CONTINUOUS; + case 1: + return Opcode.TEXT; + case 2: + return Opcode.BINARY; + // 3-7 are not yet defined + case 8: + return Opcode.CLOSING; + case 9: + return Opcode.PING; + case 10: + return Opcode.PONG; + // 11-15 are not yet defined + default: + throw new InvalidFrameException("Unknown opcode " + (short) opcode); + } + } + + @Override + public void processFrame(WebSocketImpl webSocketImpl, Framedata frame) + throws InvalidDataException { + Opcode curop = frame.getOpcode(); + if (curop == Opcode.CLOSING) { + processFrameClosing(webSocketImpl, frame); + } else if (curop == Opcode.PING) { + webSocketImpl.getWebSocketListener().onWebsocketPing(webSocketImpl, frame); + } else if (curop == Opcode.PONG) { + webSocketImpl.updateLastPong(); + webSocketImpl.getWebSocketListener().onWebsocketPong(webSocketImpl, frame); + } else if (!frame.isFin() || curop == Opcode.CONTINUOUS) { + processFrameContinuousAndNonFin(webSocketImpl, frame, curop); + } else if (currentContinuousFrame != null) { + log.error("Protocol error: Continuous frame sequence not completed."); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, + "Continuous frame sequence not completed."); + } else if (curop == Opcode.TEXT) { + processFrameText(webSocketImpl, frame); + } else if (curop == Opcode.BINARY) { + processFrameBinary(webSocketImpl, frame); + } else { + log.error("non control or continious frame expected"); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, + "non control or continious frame expected"); + } + } + + /** + * Process the frame if it is a continuous frame or the fin bit is not set + * + * @param webSocketImpl the websocket implementation to use + * @param frame the current frame + * @param curop the current Opcode + * @throws InvalidDataException if there is a protocol error + */ + private void processFrameContinuousAndNonFin(WebSocketImpl webSocketImpl, Framedata frame, + Opcode curop) throws InvalidDataException { + if (curop != Opcode.CONTINUOUS) { + processFrameIsNotFin(frame); + } else if (frame.isFin()) { + processFrameIsFin(webSocketImpl, frame); + } else if (currentContinuousFrame == null) { + log.error("Protocol error: Continuous frame sequence was not started."); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, + "Continuous frame sequence was not started."); + } + //Check if the whole payload is valid utf8, when the opcode indicates a text + if (curop == Opcode.TEXT && !Charsetfunctions.isValidUTF8(frame.getPayloadData())) { + log.error("Protocol error: Payload is not UTF8"); + throw new InvalidDataException(CloseFrame.NO_UTF8); + } + //Checking if the current continuous frame contains a correct payload with the other frames combined + if (curop == Opcode.CONTINUOUS && currentContinuousFrame != null) { + addToBufferList(frame.getPayloadData()); + } + } + + /** + * Process the frame if it is a binary frame + * + * @param webSocketImpl the websocket impl + * @param frame the frame + */ + private void processFrameBinary(WebSocketImpl webSocketImpl, Framedata frame) { + try { + webSocketImpl.getWebSocketListener() + .onWebsocketMessage(webSocketImpl, frame.getPayloadData()); + } catch (RuntimeException e) { + logRuntimeException(webSocketImpl, e); + } + } + + /** + * Log the runtime exception to the specific WebSocketImpl + * + * @param webSocketImpl the implementation of the websocket + * @param e the runtime exception + */ + private void logRuntimeException(WebSocketImpl webSocketImpl, RuntimeException e) { + log.error("Runtime exception during onWebsocketMessage", e); + webSocketImpl.getWebSocketListener().onWebsocketError(webSocketImpl, e); + } + + /** + * Process the frame if it is a text frame + * + * @param webSocketImpl the websocket impl + * @param frame the frame + */ + private void processFrameText(WebSocketImpl webSocketImpl, Framedata frame) + throws InvalidDataException { + try { + webSocketImpl.getWebSocketListener() + .onWebsocketMessage(webSocketImpl, Charsetfunctions.stringUtf8(frame.getPayloadData())); + } catch (RuntimeException e) { + logRuntimeException(webSocketImpl, e); + } + } + + /** + * Process the frame if it is the last frame + * + * @param webSocketImpl the websocket impl + * @param frame the frame + * @throws InvalidDataException if there is a protocol error + */ + private void processFrameIsFin(WebSocketImpl webSocketImpl, Framedata frame) + throws InvalidDataException { + if (currentContinuousFrame == null) { + log.trace("Protocol error: Previous continuous frame sequence not completed."); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, + "Continuous frame sequence was not started."); + } + addToBufferList(frame.getPayloadData()); + checkBufferLimit(); + if (currentContinuousFrame.getOpcode() == Opcode.TEXT) { + ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList()); + ((FramedataImpl1) currentContinuousFrame).isValid(); + try { + webSocketImpl.getWebSocketListener().onWebsocketMessage(webSocketImpl, + Charsetfunctions.stringUtf8(currentContinuousFrame.getPayloadData())); + } catch (RuntimeException e) { + logRuntimeException(webSocketImpl, e); + } + } else if (currentContinuousFrame.getOpcode() == Opcode.BINARY) { + ((FramedataImpl1) currentContinuousFrame).setPayload(getPayloadFromByteBufferList()); + ((FramedataImpl1) currentContinuousFrame).isValid(); + try { + webSocketImpl.getWebSocketListener() + .onWebsocketMessage(webSocketImpl, currentContinuousFrame.getPayloadData()); + } catch (RuntimeException e) { + logRuntimeException(webSocketImpl, e); + } + } + currentContinuousFrame = null; + clearBufferList(); + } + + /** + * Process the frame if it is not the last frame + * + * @param frame the frame + * @throws InvalidDataException if there is a protocol error + */ + private void processFrameIsNotFin(Framedata frame) throws InvalidDataException { + if (currentContinuousFrame != null) { + log.trace("Protocol error: Previous continuous frame sequence not completed."); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, + "Previous continuous frame sequence not completed."); + } + currentContinuousFrame = frame; + addToBufferList(frame.getPayloadData()); + checkBufferLimit(); + } + + /** + * Process the frame if it is a closing frame + * + * @param webSocketImpl the websocket impl + * @param frame the frame + */ + private void processFrameClosing(WebSocketImpl webSocketImpl, Framedata frame) { + int code = CloseFrame.NOCODE; + String reason = ""; + if (frame instanceof CloseFrame) { + CloseFrame cf = (CloseFrame) frame; + code = cf.getCloseCode(); + reason = cf.getMessage(); + } + if (webSocketImpl.getReadyState() == ReadyState.CLOSING) { + // complete the close handshake by disconnecting + webSocketImpl.closeConnection(code, reason, true); + } else { + // echo close handshake + if (getCloseHandshakeType() == CloseHandshakeType.TWOWAY) { + webSocketImpl.close(code, reason, true); + } else { + webSocketImpl.flushAndClose(code, reason, false); + } + } + } + + /** + * Clear the current bytebuffer list + */ + private void clearBufferList() { + synchronized (byteBufferList) { + byteBufferList.clear(); + } + } + + /** + * Add a payload to the current bytebuffer list + * + * @param payloadData the new payload + */ + private void addToBufferList(ByteBuffer payloadData) { + synchronized (byteBufferList) { + byteBufferList.add(payloadData); + } + } + + /** + * Check the current size of the buffer and throw an exception if the size is bigger than the max + * allowed frame size + * + * @throws LimitExceededException if the current size is bigger than the allowed size + */ + private void checkBufferLimit() throws LimitExceededException { + long totalSize = getByteBufferListSize(); + if (totalSize > maxFrameSize) { + clearBufferList(); + log.trace("Payload limit reached. Allowed: {} Current: {}", maxFrameSize, totalSize); + throw new LimitExceededException(maxFrameSize); + } + } + + @Override + public CloseHandshakeType getCloseHandshakeType() { + return CloseHandshakeType.TWOWAY; + } + + @Override + public String toString() { + String result = super.toString(); + if (getExtension() != null) { + result += " extension: " + getExtension().toString(); + } + if (getProtocol() != null) { + result += " protocol: " + getProtocol().toString(); + } + result += " max frame size: " + this.maxFrameSize; + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Draft_6455 that = (Draft_6455) o; + + if (maxFrameSize != that.getMaxFrameSize()) { + return false; + } + if (negotiatedExtension != null ? !negotiatedExtension.equals(that.getExtension()) : that.getExtension() != null) { + return false; + } + return protocol != null ? protocol.equals(that.getProtocol()) : that.getProtocol() == null; + } + + @Override + public int hashCode() { + int result = negotiatedExtension != null ? negotiatedExtension.hashCode() : 0; + result = 31 * result + (protocol != null ? protocol.hashCode() : 0); + result = 31 * result + (maxFrameSize ^ (maxFrameSize >>> 32)); + return result; + } + + /** + * Method to generate a full bytebuffer out of all the fragmented frame payload + * + * @return a bytebuffer containing all the data + * @throws LimitExceededException will be thrown when the totalSize is bigger then + * Integer.MAX_VALUE due to not being able to allocate more + */ + private ByteBuffer getPayloadFromByteBufferList() throws LimitExceededException { + long totalSize = 0; + ByteBuffer resultingByteBuffer; + synchronized (byteBufferList) { + for (ByteBuffer buffer : byteBufferList) { + totalSize += buffer.limit(); + } + checkBufferLimit(); + resultingByteBuffer = ByteBuffer.allocate((int) totalSize); + for (ByteBuffer buffer : byteBufferList) { + resultingByteBuffer.put(buffer); + } + } + resultingByteBuffer.flip(); + return resultingByteBuffer; + } + + /** + * Get the current size of the resulting bytebuffer in the bytebuffer list + * + * @return the size as long (to not get an integer overflow) + */ + private long getByteBufferListSize() { + long totalSize = 0; + synchronized (byteBufferList) { + for (ByteBuffer buffer : byteBufferList) { + totalSize += buffer.limit(); + } + } + return totalSize; + } + + private class TranslatedPayloadMetaData { + + private int payloadLength; + private int realPackageSize; + + private int getPayloadLength() { + return payloadLength; + } + + private int getRealPackageSize() { + return realPackageSize; + } + + TranslatedPayloadMetaData(int newPayloadLength, int newRealPackageSize) { + this.payloadLength = newPayloadLength; + this.realPackageSize = newRealPackageSize; + } + } +} diff --git a/src/main/java/org/java_websocket/drafts/package-info.java b/src/main/java/org/java_websocket/drafts/package-info.java new file mode 100644 index 0000000..1311429 --- /dev/null +++ b/src/main/java/org/java_websocket/drafts/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all implementations in relation with the WebSocket drafts. + */ +package org.java_websocket.drafts; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/CloseHandshakeType.java b/src/main/java/org/java_websocket/enums/CloseHandshakeType.java new file mode 100644 index 0000000..0bd1f94 --- /dev/null +++ b/src/main/java/org/java_websocket/enums/CloseHandshakeType.java @@ -0,0 +1,8 @@ +package org.java_websocket.enums; + +/** + * Enum which represents type of handshake is required for a close + */ +public enum CloseHandshakeType { + NONE, ONEWAY, TWOWAY +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/HandshakeState.java b/src/main/java/org/java_websocket/enums/HandshakeState.java new file mode 100644 index 0000000..c877506 --- /dev/null +++ b/src/main/java/org/java_websocket/enums/HandshakeState.java @@ -0,0 +1,15 @@ +package org.java_websocket.enums; + +/** + * Enum which represents the states a handshake may be in + */ +public enum HandshakeState { + /** + * Handshake matched this Draft successfully + */ + MATCHED, + /** + * Handshake is does not match this Draft + */ + NOT_MATCHED +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/Opcode.java b/src/main/java/org/java_websocket/enums/Opcode.java new file mode 100644 index 0000000..cc65e31 --- /dev/null +++ b/src/main/java/org/java_websocket/enums/Opcode.java @@ -0,0 +1,9 @@ +package org.java_websocket.enums; + +/** + * Enum which contains the different valid opcodes + */ +public enum Opcode { + CONTINUOUS, TEXT, BINARY, PING, PONG, CLOSING + // more to come +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/ReadyState.java b/src/main/java/org/java_websocket/enums/ReadyState.java new file mode 100644 index 0000000..15bd5cc --- /dev/null +++ b/src/main/java/org/java_websocket/enums/ReadyState.java @@ -0,0 +1,8 @@ +package org.java_websocket.enums; + +/** + * Enum which represents the state a websocket may be in + */ +public enum ReadyState { + NOT_YET_CONNECTED, OPEN, CLOSING, CLOSED +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/Role.java b/src/main/java/org/java_websocket/enums/Role.java new file mode 100644 index 0000000..8a057a3 --- /dev/null +++ b/src/main/java/org/java_websocket/enums/Role.java @@ -0,0 +1,8 @@ +package org.java_websocket.enums; + +/** + * Enum which represents the states a websocket may be in + */ +public enum Role { + CLIENT, SERVER +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/enums/package-info.java b/src/main/java/org/java_websocket/enums/package-info.java new file mode 100644 index 0000000..a5c997e --- /dev/null +++ b/src/main/java/org/java_websocket/enums/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all enums. + */ +package org.java_websocket.enums; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/exceptions/IncompleteException.java b/src/main/java/org/java_websocket/exceptions/IncompleteException.java new file mode 100644 index 0000000..d3e8383 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/IncompleteException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +/** + * Exception which indicates that the frame is not yet complete + */ +public class IncompleteException extends Exception { + + /** + * It's Serializable. + */ + private static final long serialVersionUID = 7330519489840500997L; + + /** + * The preferred size + */ + private final int preferredSize; + + /** + * Constructor for the preferred size of a frame + * + * @param preferredSize the preferred size of a frame + */ + public IncompleteException(int preferredSize) { + this.preferredSize = preferredSize; + } + + /** + * Getter for the preferredSize + * + * @return the value of the preferred size + */ + public int getPreferredSize() { + return preferredSize; + } +} diff --git a/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java b/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java new file mode 100644 index 0000000..17307c3 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/IncompleteHandshakeException.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +/** + * exception which indicates that a incomplete handshake was received + */ +public class IncompleteHandshakeException extends RuntimeException { + + /** + * Serializable + */ + private static final long serialVersionUID = 7906596804233893092L; + + /** + * attribute which size of handshake would have been preferred + */ + private final int preferredSize; + + /** + * constructor for a IncompleteHandshakeException + *

+ * + * @param preferredSize the preferred size + */ + public IncompleteHandshakeException(int preferredSize) { + this.preferredSize = preferredSize; + } + + /** + * constructor for a IncompleteHandshakeException + *

+ * preferredSize will be 0 + */ + public IncompleteHandshakeException() { + this.preferredSize = 0; + } + + /** + * Getter preferredSize + * + * @return the preferredSize + */ + public int getPreferredSize() { + return preferredSize; + } + +} diff --git a/src/main/java/org/java_websocket/exceptions/InvalidDataException.java b/src/main/java/org/java_websocket/exceptions/InvalidDataException.java new file mode 100644 index 0000000..c34c8c9 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/InvalidDataException.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +/** + * exception which indicates that a invalid data was received + */ +public class InvalidDataException extends Exception { + + /** + * Serializable + */ + private static final long serialVersionUID = 3731842424390998726L; + + /** + * attribute which closecode will be returned + */ + private final int closecode; + + /** + * constructor for a InvalidDataException + * + * @param closecode the closecode which will be returned + */ + public InvalidDataException(int closecode) { + this.closecode = closecode; + } + + /** + * constructor for a InvalidDataException. + * + * @param closecode the closecode which will be returned. + * @param s the detail message. + */ + public InvalidDataException(int closecode, String s) { + super(s); + this.closecode = closecode; + } + + /** + * constructor for a InvalidDataException. + * + * @param closecode the closecode which will be returned. + * @param t the throwable causing this exception. + */ + public InvalidDataException(int closecode, Throwable t) { + super(t); + this.closecode = closecode; + } + + /** + * constructor for a InvalidDataException. + * + * @param closecode the closecode which will be returned. + * @param s the detail message. + * @param t the throwable causing this exception. + */ + public InvalidDataException(int closecode, String s, Throwable t) { + super(s, t); + this.closecode = closecode; + } + + /** + * Getter closecode + * + * @return the closecode + */ + public int getCloseCode() { + return closecode; + } + +} diff --git a/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java b/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java new file mode 100644 index 0000000..8fdbd1a --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/InvalidEncodingException.java @@ -0,0 +1,37 @@ +package org.java_websocket.exceptions; + +import java.io.UnsupportedEncodingException; + +/** + * The Character Encoding is not supported. + * + * @since 1.4.0 + */ +public class InvalidEncodingException extends RuntimeException { + + /** + * attribute for the encoding exception + */ + private final UnsupportedEncodingException encodingException; + + /** + * constructor for InvalidEncodingException + * + * @param encodingException the cause for this exception + */ + public InvalidEncodingException(UnsupportedEncodingException encodingException) { + if (encodingException == null) { + throw new IllegalArgumentException(); + } + this.encodingException = encodingException; + } + + /** + * Get the exception which includes more information on the unsupported encoding + * + * @return an UnsupportedEncodingException + */ + public UnsupportedEncodingException getEncodingException() { + return encodingException; + } +} diff --git a/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java b/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java new file mode 100644 index 0000000..7de2034 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/InvalidFrameException.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +import org.java_websocket.framing.CloseFrame; + +/** + * exception which indicates that a invalid frame was received (CloseFrame.PROTOCOL_ERROR) + */ +public class InvalidFrameException extends InvalidDataException { + + /** + * Serializable + */ + private static final long serialVersionUID = -9016496369828887591L; + + /** + * constructor for a InvalidFrameException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + */ + public InvalidFrameException() { + super(CloseFrame.PROTOCOL_ERROR); + } + + /** + * constructor for a InvalidFrameException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param s the detail message. + */ + public InvalidFrameException(String s) { + super(CloseFrame.PROTOCOL_ERROR, s); + } + + /** + * constructor for a InvalidFrameException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param t the throwable causing this exception. + */ + public InvalidFrameException(Throwable t) { + super(CloseFrame.PROTOCOL_ERROR, t); + } + + /** + * constructor for a InvalidFrameException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param s the detail message. + * @param t the throwable causing this exception. + */ + public InvalidFrameException(String s, Throwable t) { + super(CloseFrame.PROTOCOL_ERROR, s, t); + } +} diff --git a/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java b/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java new file mode 100644 index 0000000..af1fd21 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/InvalidHandshakeException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +import org.java_websocket.framing.CloseFrame; + +/** + * exception which indicates that a invalid handshake was received (CloseFrame.PROTOCOL_ERROR) + */ +public class InvalidHandshakeException extends InvalidDataException { + + /** + * Serializable + */ + private static final long serialVersionUID = -1426533877490484964L; + + /** + * constructor for a InvalidHandshakeException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + */ + public InvalidHandshakeException() { + super(CloseFrame.PROTOCOL_ERROR); + } + + /** + * constructor for a InvalidHandshakeException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param s the detail message. + * @param t the throwable causing this exception. + */ + public InvalidHandshakeException(String s, Throwable t) { + super(CloseFrame.PROTOCOL_ERROR, s, t); + } + + /** + * constructor for a InvalidHandshakeException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param s the detail message. + */ + public InvalidHandshakeException(String s) { + super(CloseFrame.PROTOCOL_ERROR, s); + } + + /** + * constructor for a InvalidHandshakeException + *

+ * calling InvalidDataException with closecode PROTOCOL_ERROR + * + * @param t the throwable causing this exception. + */ + public InvalidHandshakeException(Throwable t) { + super(CloseFrame.PROTOCOL_ERROR, t); + } + +} diff --git a/src/main/java/org/java_websocket/exceptions/LimitExceededException.java b/src/main/java/org/java_websocket/exceptions/LimitExceededException.java new file mode 100644 index 0000000..0d4a818 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/LimitExceededException.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +import org.java_websocket.framing.CloseFrame; + +/** + * exception which indicates that the message limited was exceeded (CloseFrame.TOOBIG) + */ +public class LimitExceededException extends InvalidDataException { + + /** + * Serializable + */ + private static final long serialVersionUID = 6908339749836826785L; + + /** + * A closer indication about the limit + */ + private final int limit; + + /** + * constructor for a LimitExceededException + *

+ * calling LimitExceededException with closecode TOOBIG + */ + public LimitExceededException() { + this(Integer.MAX_VALUE); + } + + /** + * constructor for a LimitExceededException + *

+ * calling InvalidDataException with closecode TOOBIG + * @param limit the allowed size which was not enough + */ + public LimitExceededException(int limit) { + super(CloseFrame.TOOBIG); + this.limit = limit; + } + + /** + * constructor for a LimitExceededException + *

+ * calling InvalidDataException with closecode TOOBIG + * @param s the detail message. + * @param limit the allowed size which was not enough + */ + public LimitExceededException(String s, int limit) { + super(CloseFrame.TOOBIG, s); + this.limit = limit; + } + + /** + * constructor for a LimitExceededException + *

+ * calling InvalidDataException with closecode TOOBIG + * + * @param s the detail message. + */ + public LimitExceededException(String s) { + this(s, Integer.MAX_VALUE); + } + + /** + * Get the limit which was hit so this exception was caused + * + * @return the limit as int + */ + public int getLimit() { + return limit; + } +} diff --git a/src/main/java/org/java_websocket/exceptions/NotSendableException.java b/src/main/java/org/java_websocket/exceptions/NotSendableException.java new file mode 100644 index 0000000..fbacca9 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/NotSendableException.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +/** + * exception which indicates the frame payload is not sendable + */ +public class NotSendableException extends RuntimeException { + + /** + * Serializable + */ + private static final long serialVersionUID = -6468967874576651628L; + + /** + * constructor for a NotSendableException + * + * @param s the detail message. + */ + public NotSendableException(String s) { + super(s); + } + + /** + * constructor for a NotSendableException + * + * @param t the throwable causing this exception. + */ + public NotSendableException(Throwable t) { + super(t); + } + + /** + * constructor for a NotSendableException + * + * @param s the detail message. + * @param t the throwable causing this exception. + */ + public NotSendableException(String s, Throwable t) { + super(s, t); + } + +} diff --git a/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java b/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java new file mode 100644 index 0000000..082f0bd --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/WebsocketNotConnectedException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.exceptions; + +/** + * exception which indicates the websocket is not yet connected (ReadyState.OPEN) + */ +public class WebsocketNotConnectedException extends RuntimeException { + + /** + * Serializable + */ + private static final long serialVersionUID = -785314021592982715L; +} diff --git a/src/main/java/org/java_websocket/exceptions/WrappedIOException.java b/src/main/java/org/java_websocket/exceptions/WrappedIOException.java new file mode 100644 index 0000000..3ec3177 --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/WrappedIOException.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +package org.java_websocket.exceptions; + +import java.io.IOException; +import org.java_websocket.WebSocket; + +/** + * Exception to wrap an IOException and include information about the websocket which had the + * exception + * + * @since 1.4.1 + */ +public class WrappedIOException extends Exception { + + /** + * The websocket where the IOException happened + */ + private final transient WebSocket connection; + + /** + * The IOException + */ + private final IOException ioException; + + /** + * Wrapp an IOException and include the websocket + * + * @param connection the websocket where the IOException happened + * @param ioException the IOException + */ + public WrappedIOException(WebSocket connection, IOException ioException) { + this.connection = connection; + this.ioException = ioException; + } + + /** + * The websocket where the IOException happened + * + * @return the websocket for the wrapped IOException + */ + public WebSocket getConnection() { + return connection; + } + + /** + * The wrapped IOException + * + * @return IOException which is wrapped + */ + public IOException getIOException() { + return ioException; + } +} diff --git a/src/main/java/org/java_websocket/exceptions/package-info.java b/src/main/java/org/java_websocket/exceptions/package-info.java new file mode 100644 index 0000000..2972d3c --- /dev/null +++ b/src/main/java/org/java_websocket/exceptions/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all implementations in relation with the exceptions thrown in this + * lib. + */ +package org.java_websocket.exceptions; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/extensions/CompressionExtension.java b/src/main/java/org/java_websocket/extensions/CompressionExtension.java new file mode 100644 index 0000000..408a158 --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/CompressionExtension.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.extensions; + +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.framing.ControlFrame; +import org.java_websocket.framing.DataFrame; +import org.java_websocket.framing.Framedata; + +/** + * Implementation for a compression extension specified by https://tools.ietf.org/html/rfc7692 + * + * @since 1.3.5 + */ +public abstract class CompressionExtension extends DefaultExtension { + + @Override + public void isFrameValid(Framedata inputFrame) throws InvalidDataException { + if ((inputFrame instanceof DataFrame) && (inputFrame.isRSV2() || inputFrame.isRSV3())) { + throw new InvalidFrameException( + "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + + inputFrame.isRSV3()); + } + if ((inputFrame instanceof ControlFrame) && (inputFrame.isRSV1() || inputFrame.isRSV2() + || inputFrame.isRSV3())) { + throw new InvalidFrameException( + "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + + inputFrame.isRSV3()); + } + } +} diff --git a/src/main/java/org/java_websocket/extensions/DefaultExtension.java b/src/main/java/org/java_websocket/extensions/DefaultExtension.java new file mode 100644 index 0000000..3892990 --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/DefaultExtension.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.extensions; + +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.framing.Framedata; + +/** + * Class which represents the normal websocket implementation specified by rfc6455. + *

+ * This is a fallback and will always be available for a Draft_6455 + * + * @since 1.3.5 + */ +public class DefaultExtension implements IExtension { + + @Override + public void decodeFrame(Framedata inputFrame) throws InvalidDataException { + //Nothing to do here + } + + @Override + public void encodeFrame(Framedata inputFrame) { + //Nothing to do here + } + + @Override + public boolean acceptProvidedExtensionAsServer(String inputExtension) { + return true; + } + + @Override + public boolean acceptProvidedExtensionAsClient(String inputExtension) { + return true; + } + + @Override + public void isFrameValid(Framedata inputFrame) throws InvalidDataException { + if (inputFrame.isRSV1() || inputFrame.isRSV2() || inputFrame.isRSV3()) { + throw new InvalidFrameException( + "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + + inputFrame.isRSV3()); + } + } + + @Override + public String getProvidedExtensionAsClient() { + return ""; + } + + @Override + public String getProvidedExtensionAsServer() { + return ""; + } + + @Override + public IExtension copyInstance() { + return new DefaultExtension(); + } + + public void reset() { + //Nothing to do here. No internal stats. + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o || o != null && getClass() == o.getClass(); + } +} diff --git a/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java b/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java new file mode 100644 index 0000000..37bc9de --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/ExtensionRequestData.java @@ -0,0 +1,53 @@ +package org.java_websocket.extensions; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ExtensionRequestData { + + public static final String EMPTY_VALUE = ""; + + private Map extensionParameters; + private String extensionName; + + private ExtensionRequestData() { + extensionParameters = new LinkedHashMap<>(); + } + + public static ExtensionRequestData parseExtensionRequest(String extensionRequest) { + ExtensionRequestData extensionData = new ExtensionRequestData(); + String[] parts = extensionRequest.split(";"); + extensionData.extensionName = parts[0].trim(); + + for (int i = 1; i < parts.length; i++) { + String[] keyValue = parts[i].split("="); + String value = EMPTY_VALUE; + + // Some parameters don't take a value. For those that do, parse the value. + if (keyValue.length > 1) { + String tempValue = keyValue[1].trim(); + + // If the value is wrapped in quotes, just get the data between them. + if ((tempValue.startsWith("\"") && tempValue.endsWith("\"")) + || (tempValue.startsWith("'") && tempValue.endsWith("'")) + && tempValue.length() > 2) { + tempValue = tempValue.substring(1, tempValue.length() - 1); + } + + value = tempValue; + } + + extensionData.extensionParameters.put(keyValue[0].trim(), value); + } + + return extensionData; + } + + public String getExtensionName() { + return extensionName; + } + + public Map getExtensionParameters() { + return extensionParameters; + } +} diff --git a/src/main/java/org/java_websocket/extensions/IExtension.java b/src/main/java/org/java_websocket/extensions/IExtension.java new file mode 100644 index 0000000..02bf581 --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/IExtension.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.extensions; + +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.framing.Framedata; + +/** + * Interface which specifies all required methods to develop a websocket extension. + * + * @since 1.3.5 + */ +public interface IExtension { + + /** + * Decode a frame with a extension specific algorithm. The algorithm is subject to be implemented + * by the specific extension. The resulting frame will be used in the application + * + * @param inputFrame the frame, which has do be decoded to be used in the application + * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly + * implemented by the other endpoint or there are other protocol + * errors/decoding errors + * @since 1.3.5 + */ + void decodeFrame(Framedata inputFrame) throws InvalidDataException; + + /** + * Encode a frame with a extension specific algorithm. The algorithm is subject to be implemented + * by the specific extension. The resulting frame will be send to the other endpoint. + * + * @param inputFrame the frame, which has do be encoded to be used on the other endpoint + * @since 1.3.5 + */ + void encodeFrame(Framedata inputFrame); + + /** + * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific + * extension if the endpoint is in the role of a server + * + * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the + * other endpoint + * @return true, if the offer does fit to this specific extension + * @since 1.3.5 + */ + boolean acceptProvidedExtensionAsServer(String inputExtensionHeader); + + /** + * Check if the received Sec-WebSocket-Extensions header field contains a offer for the specific + * extension if the endpoint is in the role of a client + * + * @param inputExtensionHeader the received Sec-WebSocket-Extensions header field offered by the + * other endpoint + * @return true, if the offer does fit to this specific extension + * @since 1.3.5 + */ + boolean acceptProvidedExtensionAsClient(String inputExtensionHeader); + + /** + * Check if the received frame is correctly implemented by the other endpoint and there are no + * specification errors (like wrongly set RSV) + * + * @param inputFrame the received frame + * @throws InvalidDataException Throw InvalidDataException if the received frame is not correctly + * implementing the specification for the specific extension + * @since 1.3.5 + */ + void isFrameValid(Framedata inputFrame) throws InvalidDataException; + + /** + * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is + * in the role of a client. If the extension returns an empty string (""), the offer will not be + * included in the handshake. + * + * @return the specific Sec-WebSocket-Extensions header for this extension + * @since 1.3.5 + */ + String getProvidedExtensionAsClient(); + + /** + * Return the specific Sec-WebSocket-Extensions header offer for this extension if the endpoint is + * in the role of a server. If the extension returns an empty string (""), the offer will not be + * included in the handshake. + * + * @return the specific Sec-WebSocket-Extensions header for this extension + * @since 1.3.5 + */ + String getProvidedExtensionAsServer(); + + /** + * Extensions must only be by one websocket at all. To prevent extensions to be used more than + * once the Websocket implementation should call this method in order to create a new usable + * version of a given extension instance.
The copy can be safely used in conjunction with a + * new websocket connection. + * + * @return a copy of the extension + * @since 1.3.5 + */ + IExtension copyInstance(); + + /** + * Cleaning up internal stats when the draft gets reset. + * + * @since 1.3.5 + */ + void reset(); + + /** + * Return a string which should contain the class name as well as additional information about the + * current configurations for this extension (DEBUG purposes) + * + * @return a string containing the class name as well as additional information + * @since 1.3.5 + */ + String toString(); +} diff --git a/src/main/java/org/java_websocket/extensions/package-info.java b/src/main/java/org/java_websocket/extensions/package-info.java new file mode 100644 index 0000000..251cbdf --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all interfaces and implementations in relation with the WebSocket + * Sec-WebSocket-Extensions. + */ +package org.java_websocket.extensions; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java b/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java new file mode 100644 index 0000000..4027abf --- /dev/null +++ b/src/main/java/org/java_websocket/extensions/permessage_deflate/PerMessageDeflateExtension.java @@ -0,0 +1,372 @@ +package org.java_websocket.extensions.permessage_deflate; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.extensions.CompressionExtension; +import org.java_websocket.extensions.ExtensionRequestData; +import org.java_websocket.extensions.IExtension; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.ContinuousFrame; +import org.java_websocket.framing.DataFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.FramedataImpl1; + +/** + * PerMessage Deflate Extension (7. The + * "permessage-deflate" Extension in + * RFC 7692). + * + * @see 7. The "permessage-deflate" + * Extension in RFC 7692 + */ +public class PerMessageDeflateExtension extends CompressionExtension { + + // Name of the extension as registered by IETF https://tools.ietf.org/html/rfc7692#section-9. + private static final String EXTENSION_REGISTERED_NAME = "permessage-deflate"; + // Below values are defined for convenience. They are not used in the compression/decompression phase. + // They may be needed during the extension-negotiation offer in the future. + private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover"; + private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover"; + private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits"; + private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits"; + private static final int serverMaxWindowBits = 1 << 15; + private static final int clientMaxWindowBits = 1 << 15; + private static final byte[] TAIL_BYTES = {(byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF}; + private static final int BUFFER_SIZE = 1 << 10; + + private int threshold = 1024; + + private boolean serverNoContextTakeover = true; + private boolean clientNoContextTakeover = false; + + // For WebSocketServers, this variable holds the extension parameters that the peer client has requested. + // For WebSocketClients, this variable holds the extension parameters that client himself has requested. + private Map requestedParameters = new LinkedHashMap<>(); + + private final int compressionLevel; + + private final Inflater inflater; + private final Deflater deflater; + + /** + * Constructor for the PerMessage Deflate Extension (7. Thepermessage-deflate" Extension) + * + * Uses {@link Deflater#DEFAULT_COMPRESSION} as the compression level for the {@link Deflater#Deflater(int)} + */ + public PerMessageDeflateExtension() { + this(Deflater.DEFAULT_COMPRESSION); + } + + /** + * Constructor for the PerMessage Deflate Extension (7. Thepermessage-deflate" Extension) + * + * @param compressionLevel The compression level passed to the {@link Deflater#Deflater(int)} + */ + public PerMessageDeflateExtension(int compressionLevel) { + this.compressionLevel = compressionLevel; + this.deflater = new Deflater(this.compressionLevel, true); + this.inflater = new Inflater(true); + } + + /** + * Get the compression level used for the compressor. + * @return the compression level + */ + public int getCompressionLevel() { + return this.compressionLevel; + } + + /** + * Get the size threshold for doing the compression + * @return Size (in bytes) below which messages will not be compressed + * @since 1.5.3 + */ + public int getThreshold() { + return threshold; + } + + /** + * Set the size when payloads smaller than this will not be compressed. + * @param threshold the size in bytes + * @since 1.5.3 + */ + public void setThreshold(int threshold) { + this.threshold = threshold; + } + + /** + * Access the "server_no_context_takeover" extension parameter + * + * @see The "server_no_context_takeover" Extension Parameter + * @return serverNoContextTakeover is the server no context parameter active + */ + public boolean isServerNoContextTakeover() { + return serverNoContextTakeover; + } + + /** + * Setter for the "server_no_context_takeover" extension parameter + * @see The "server_no_context_takeover" Extension Parameter + * @param serverNoContextTakeover set the server no context parameter + */ + public void setServerNoContextTakeover(boolean serverNoContextTakeover) { + this.serverNoContextTakeover = serverNoContextTakeover; + } + + /** + * Access the "client_no_context_takeover" extension parameter + * + * @see The "client_no_context_takeover" Extension Parameter + * @return clientNoContextTakeover is the client no context parameter active + */ + public boolean isClientNoContextTakeover() { + return clientNoContextTakeover; + } + + /** + * Setter for the "client_no_context_takeover" extension parameter + * @see The "client_no_context_takeover" Extension Parameter + * @param clientNoContextTakeover set the client no context parameter + */ + public void setClientNoContextTakeover(boolean clientNoContextTakeover) { + this.clientNoContextTakeover = clientNoContextTakeover; + } + + /* + An endpoint uses the following algorithm to decompress a message. + 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the + payload of the message. + 2. Decompress the resulting data using DEFLATE. + See, https://tools.ietf.org/html/rfc7692#section-7.2.2 + */ + @Override + public void decodeFrame(Framedata inputFrame) throws InvalidDataException { + // Only DataFrames can be decompressed. + if (!(inputFrame instanceof DataFrame)) { + return; + } + + if (!inputFrame.isRSV1() && inputFrame.getOpcode() != Opcode.CONTINUOUS) { + return; + } + + // RSV1 bit must be set only for the first frame. + if (inputFrame.getOpcode() == Opcode.CONTINUOUS && inputFrame.isRSV1()) { + throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, + "RSV1 bit can only be set for the first frame."); + } + + // Decompressed output buffer. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + decompress(inputFrame.getPayloadData().array(), output); + + /* + If a message is "first fragmented and then compressed", as this project does, then the inflater + can not inflate fragments except the first one. + This behavior occurs most likely because those fragments end with "final deflate blocks". + We can check the getRemaining() method to see whether the data we supplied has been decompressed or not. + And if not, we just reset the inflater and decompress again. + Note that this behavior doesn't occur if the message is "first compressed and then fragmented". + */ + if (inflater.getRemaining() > 0) { + inflater.reset(); + decompress(inputFrame.getPayloadData().array(), output); + } + + if (inputFrame.isFin()) { + decompress(TAIL_BYTES, output); + // If context takeover is disabled, inflater can be reset. + if (clientNoContextTakeover) { + inflater.reset(); + } + } + } catch (DataFormatException e) { + throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, e.getMessage()); + } + + // Set frames payload to the new decompressed data. + ((FramedataImpl1) inputFrame) + .setPayload(ByteBuffer.wrap(output.toByteArray(), 0, output.size())); + } + + /** + * @param data the bytes of data + * @param outputBuffer the output stream + * @throws DataFormatException + */ + private void decompress(byte[] data, ByteArrayOutputStream outputBuffer) + throws DataFormatException { + inflater.setInput(data); + byte[] buffer = new byte[BUFFER_SIZE]; + + int bytesInflated; + while ((bytesInflated = inflater.inflate(buffer)) > 0) { + outputBuffer.write(buffer, 0, bytesInflated); + } + } + + @Override + public void encodeFrame(Framedata inputFrame) { + // Only DataFrames can be decompressed. + if (!(inputFrame instanceof DataFrame)) { + return; + } + + byte[] payloadData = inputFrame.getPayloadData().array(); + if (payloadData.length < threshold) { + return; + } + // Only the first frame's RSV1 must be set. + if (!(inputFrame instanceof ContinuousFrame)) { + ((DataFrame) inputFrame).setRSV1(true); + } + + deflater.setInput(payloadData); + // Compressed output buffer. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + // Temporary buffer to hold compressed output. + byte[] buffer = new byte[1024]; + int bytesCompressed; + while ((bytesCompressed = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)) + > 0) { + output.write(buffer, 0, bytesCompressed); + } + + byte[] outputBytes = output.toByteArray(); + int outputLength = outputBytes.length; + + /* + https://tools.ietf.org/html/rfc7692#section-7.2.1 states that if the final fragment's compressed + payload ends with 0x00 0x00 0xff 0xff, they should be removed. + To simulate removal, we just pass 4 bytes less to the new payload + if the frame is final and outputBytes ends with 0x00 0x00 0xff 0xff. + */ + if (inputFrame.isFin()) { + if (endsWithTail(outputBytes)) { + outputLength -= TAIL_BYTES.length; + } + + if (serverNoContextTakeover) { + deflater.reset(); + } + } + + // Set frames payload to the new compressed data. + ((FramedataImpl1) inputFrame).setPayload(ByteBuffer.wrap(outputBytes, 0, outputLength)); + } + + /** + * @param data the bytes of data + * @return true if the data is OK + */ + private static boolean endsWithTail(byte[] data) { + if (data.length < 4) { + return false; + } + + int length = data.length; + for (int i = 0; i < TAIL_BYTES.length; i++) { + if (TAIL_BYTES[i] != data[length - TAIL_BYTES.length + i]) { + return false; + } + } + + return true; + } + + @Override + public boolean acceptProvidedExtensionAsServer(String inputExtension) { + String[] requestedExtensions = inputExtension.split(","); + for (String extension : requestedExtensions) { + ExtensionRequestData extensionData = ExtensionRequestData.parseExtensionRequest(extension); + if (!EXTENSION_REGISTERED_NAME.equalsIgnoreCase(extensionData.getExtensionName())) { + continue; + } + + // Holds parameters that peer client has sent. + Map headers = extensionData.getExtensionParameters(); + requestedParameters.putAll(headers); + if (requestedParameters.containsKey(CLIENT_NO_CONTEXT_TAKEOVER)) { + clientNoContextTakeover = true; + } + + return true; + } + + return false; + } + + @Override + public boolean acceptProvidedExtensionAsClient(String inputExtension) { + String[] requestedExtensions = inputExtension.split(","); + for (String extension : requestedExtensions) { + ExtensionRequestData extensionData = ExtensionRequestData.parseExtensionRequest(extension); + if (!EXTENSION_REGISTERED_NAME.equalsIgnoreCase(extensionData.getExtensionName())) { + continue; + } + + // Holds parameters that are sent by the server, as a response to our initial extension request. + Map headers = extensionData.getExtensionParameters(); + // After this point, parameters that the server sent back can be configured, but we don't use them for now. + return true; + } + + return false; + } + + @Override + public String getProvidedExtensionAsClient() { + requestedParameters.put(CLIENT_NO_CONTEXT_TAKEOVER, ExtensionRequestData.EMPTY_VALUE); + requestedParameters.put(SERVER_NO_CONTEXT_TAKEOVER, ExtensionRequestData.EMPTY_VALUE); + + return EXTENSION_REGISTERED_NAME + "; " + SERVER_NO_CONTEXT_TAKEOVER + "; " + + CLIENT_NO_CONTEXT_TAKEOVER; + } + + @Override + public String getProvidedExtensionAsServer() { + return EXTENSION_REGISTERED_NAME + + "; " + SERVER_NO_CONTEXT_TAKEOVER + + (clientNoContextTakeover ? "; " + CLIENT_NO_CONTEXT_TAKEOVER : ""); + } + + @Override + public IExtension copyInstance() { + PerMessageDeflateExtension clone = new PerMessageDeflateExtension(this.getCompressionLevel()); + clone.setThreshold(this.getThreshold()); + clone.setClientNoContextTakeover(this.isClientNoContextTakeover()); + clone.setServerNoContextTakeover(this.isServerNoContextTakeover()); + return clone; + } + + /** + * This extension requires the RSV1 bit to be set only for the first frame. If the frame is type + * is CONTINUOUS, RSV1 bit must be unset. + */ + @Override + public void isFrameValid(Framedata inputFrame) throws InvalidDataException { + if ((inputFrame instanceof ContinuousFrame) && (inputFrame.isRSV1() || inputFrame.isRSV2() + || inputFrame.isRSV3())) { + throw new InvalidFrameException( + "bad rsv RSV1: " + inputFrame.isRSV1() + " RSV2: " + inputFrame.isRSV2() + " RSV3: " + + inputFrame.isRSV3()); + } + super.isFrameValid(inputFrame); + } + + @Override + public String toString() { + return "PerMessageDeflateExtension"; + } + + +} diff --git a/src/main/java/org/java_websocket/framing/BinaryFrame.java b/src/main/java/org/java_websocket/framing/BinaryFrame.java new file mode 100644 index 0000000..dc05449 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/BinaryFrame.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; + +/** + * Class to represent a binary frame + */ +public class BinaryFrame extends DataFrame { + + /** + * constructor which sets the opcode of this frame to binary + */ + public BinaryFrame() { + super(Opcode.BINARY); + } +} diff --git a/src/main/java/org/java_websocket/framing/CloseFrame.java b/src/main/java/org/java_websocket/framing/CloseFrame.java new file mode 100644 index 0000000..5a63d8a --- /dev/null +++ b/src/main/java/org/java_websocket/framing/CloseFrame.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import java.nio.ByteBuffer; +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; +import org.java_websocket.util.ByteBufferUtils; +import org.java_websocket.util.Charsetfunctions; + +/** + * Class to represent a close frame + */ +public class CloseFrame extends ControlFrame { + + /** + * indicates a normal closure, meaning whatever purpose the connection was established for has + * been fulfilled. + */ + public static final int NORMAL = 1000; + /** + * 1001 indicates that an endpoint is "going away", such as a server going down, or a browser + * having navigated away from a page. + */ + public static final int GOING_AWAY = 1001; + /** + * 1002 indicates that an endpoint is terminating the connection due to a protocol error. + */ + public static final int PROTOCOL_ERROR = 1002; + /** + * 1003 indicates that an endpoint is terminating the connection because it has received a type of + * data it cannot accept (e.g. an endpoint that understands only text data MAY send this if it + * receives a binary message). + */ + public static final int REFUSE = 1003; + /*1004: Reserved. The specific meaning might be defined in the future.*/ + /** + * 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an + * endpoint. It is designated for use in applications expecting a status code to indicate that no + * status code was actually present. + */ + public static final int NOCODE = 1005; + /** + * 1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an + * endpoint. It is designated for use in applications expecting a status code to indicate that the + * connection was closed abnormally, e.g. without sending or receiving a Close control frame. + */ + public static final int ABNORMAL_CLOSE = 1006; + /** + * 1007 indicates that an endpoint is terminating the connection because it has received data + * within a message that was not consistent with the type of the message (e.g., non-UTF-8 + * [RFC3629] data within a text message). + */ + public static final int NO_UTF8 = 1007; + /** + * 1008 indicates that an endpoint is terminating the connection because it has received a message + * that violates its policy. This is a generic status code that can be returned when there is no + * other more suitable status code (e.g. 1003 or 1009), or if there is a need to hide specific + * details about the policy. + */ + public static final int POLICY_VALIDATION = 1008; + /** + * 1009 indicates that an endpoint is terminating the connection because it has received a message + * which is too big for it to process. + */ + public static final int TOOBIG = 1009; + /** + * 1010 indicates that an endpoint (client) is terminating the connection because it has expected + * the server to negotiate one or more extension, but the server didn't return them in the + * response message of the WebSocket handshake. The list of extensions which are needed SHOULD + * appear in the /reason/ part of the Close frame. Note that this status code is not used by the + * server, because it can fail the WebSocket handshake instead. + */ + public static final int EXTENSION = 1010; + /** + * 1011 indicates that a server is terminating the connection because it encountered an unexpected + * condition that prevented it from fulfilling the request. + **/ + public static final int UNEXPECTED_CONDITION = 1011; + /** + * 1012 indicates that the service is restarted. A client may reconnect, and if it choses to do, + * should reconnect using a randomized delay of 5 - 30s. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html + * for more information. + * + * @since 1.3.8 + **/ + public static final int SERVICE_RESTART = 1012; + /** + * 1013 indicates that the service is experiencing overload. A client should only connect to a + * different IP (when there are multiple for the target) or reconnect to the same IP upon user + * action. See https://www.ietf.org/mail-archive/web/hybi/current/msg09670.html for more + * information. + * + * @since 1.3.8 + **/ + public static final int TRY_AGAIN_LATER = 1013; + /** + * 1014 indicates that the server was acting as a gateway or proxy and received an invalid + * response from the upstream server. This is similar to 502 HTTP Status Code See + * https://www.ietf.org/mail-archive/web/hybi/current/msg10748.html fore more information. + * + * @since 1.3.8 + **/ + public static final int BAD_GATEWAY = 1014; + /** + * 1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an + * endpoint. It is designated for use in applications expecting a status code to indicate that the + * connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate + * can't be verified). + **/ + public static final int TLS_ERROR = 1015; + + /** + * The connection had never been established + */ + public static final int NEVER_CONNECTED = -1; + + /** + * The connection had a buggy close (this should not happen) + */ + public static final int BUGGYCLOSE = -2; + + /** + * The connection was flushed and closed + */ + public static final int FLASHPOLICY = -3; + + + /** + * The close code used in this close frame + */ + private int code; + + /** + * The close message used in this close frame + */ + private String reason; + + /** + * Constructor for a close frame + *

+ * Using opcode closing and fin = true + */ + public CloseFrame() { + super(Opcode.CLOSING); + setReason(""); + setCode(CloseFrame.NORMAL); + } + + /** + * Set the close code for this close frame + * + * @param code the close code + */ + public void setCode(int code) { + this.code = code; + // CloseFrame.TLS_ERROR is not allowed to be transferred over the wire + if (code == CloseFrame.TLS_ERROR) { + this.code = CloseFrame.NOCODE; + this.reason = ""; + } + updatePayload(); + } + + /** + * Set the close reason for this close frame + * + * @param reason the reason code + */ + public void setReason(String reason) { + if (reason == null) { + reason = ""; + } + this.reason = reason; + updatePayload(); + } + + /** + * Get the used close code + * + * @return the used close code + */ + public int getCloseCode() { + return code; + } + + /** + * Get the message that closeframe is containing + * + * @return the message in this frame + */ + public String getMessage() { + return reason; + } + + @Override + public String toString() { + return super.toString() + "code: " + code; + } + + @Override + public void isValid() throws InvalidDataException { + super.isValid(); + if (code == CloseFrame.NO_UTF8 && reason.isEmpty()) { + throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!"); + } + if (code == CloseFrame.NOCODE && 0 < reason.length()) { + throw new InvalidDataException(PROTOCOL_ERROR, + "A close frame must have a closecode if it has a reason"); + } + //Intentional check for code != CloseFrame.TLS_ERROR just to make sure even if the code earlier changes + if ((code > CloseFrame.TLS_ERROR && code < 3000)) { + throw new InvalidDataException(PROTOCOL_ERROR, "Trying to send an illegal close code!"); + } + if (code == CloseFrame.ABNORMAL_CLOSE || code == CloseFrame.TLS_ERROR + || code == CloseFrame.NOCODE || code > 4999 || code < 1000 || code == 1004) { + throw new InvalidFrameException("closecode must not be sent over the wire: " + code); + } + } + + @Override + public void setPayload(ByteBuffer payload) { + code = CloseFrame.NOCODE; + reason = ""; + payload.mark(); + if (payload.remaining() == 0) { + code = CloseFrame.NORMAL; + } else if (payload.remaining() == 1) { + code = CloseFrame.PROTOCOL_ERROR; + } else { + if (payload.remaining() >= 2) { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.position(2); + bb.putShort(payload.getShort()); + bb.position(0); + code = bb.getInt(); + } + payload.reset(); + try { + int mark = payload.position();// because stringUtf8 also creates a mark + validateUtf8(payload, mark); + } catch (InvalidDataException e) { + code = CloseFrame.NO_UTF8; + reason = null; + } + } + } + + /** + * Validate the payload to valid utf8 + * + * @param mark the current mark + * @param payload the current payload + * @throws InvalidDataException the current payload is not a valid utf8 + */ + private void validateUtf8(ByteBuffer payload, int mark) throws InvalidDataException { + try { + payload.position(payload.position() + 2); + reason = Charsetfunctions.stringUtf8(payload); + } catch (IllegalArgumentException e) { + throw new InvalidDataException(CloseFrame.NO_UTF8); + } finally { + payload.position(mark); + } + } + + /** + * Update the payload to represent the close code and the reason + */ + private void updatePayload() { + byte[] by = Charsetfunctions.utf8Bytes(reason); + ByteBuffer buf = ByteBuffer.allocate(4); + buf.putInt(code); + buf.position(2); + ByteBuffer pay = ByteBuffer.allocate(2 + by.length); + pay.put(buf); + pay.put(by); + pay.rewind(); + super.setPayload(pay); + } + + @Override + public ByteBuffer getPayloadData() { + if (code == NOCODE) { + return ByteBufferUtils.getEmptyByteBuffer(); + } + return super.getPayloadData(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + CloseFrame that = (CloseFrame) o; + + if (code != that.code) { + return false; + } + return reason != null ? reason.equals(that.reason) : that.reason == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + code; + result = 31 * result + (reason != null ? reason.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/org/java_websocket/framing/ContinuousFrame.java b/src/main/java/org/java_websocket/framing/ContinuousFrame.java new file mode 100644 index 0000000..c518cc3 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/ContinuousFrame.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; + +/** + * Class to represent a continuous frame + */ +public class ContinuousFrame extends DataFrame { + + /** + * constructor which sets the opcode of this frame to continuous + */ + public ContinuousFrame() { + super(Opcode.CONTINUOUS); + } +} diff --git a/src/main/java/org/java_websocket/framing/ControlFrame.java b/src/main/java/org/java_websocket/framing/ControlFrame.java new file mode 100644 index 0000000..1469dc5 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/ControlFrame.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; + +/** + * Abstract class to represent control frames + */ +public abstract class ControlFrame extends FramedataImpl1 { + + /** + * Class to represent a control frame + * + * @param opcode the opcode to use + */ + public ControlFrame(Opcode opcode) { + super(opcode); + } + + @Override + public void isValid() throws InvalidDataException { + if (!isFin()) { + throw new InvalidFrameException("Control frame can't have fin==false set"); + } + if (isRSV1()) { + throw new InvalidFrameException("Control frame can't have rsv1==true set"); + } + if (isRSV2()) { + throw new InvalidFrameException("Control frame can't have rsv2==true set"); + } + if (isRSV3()) { + throw new InvalidFrameException("Control frame can't have rsv3==true set"); + } + } +} diff --git a/src/main/java/org/java_websocket/framing/DataFrame.java b/src/main/java/org/java_websocket/framing/DataFrame.java new file mode 100644 index 0000000..c845c2c --- /dev/null +++ b/src/main/java/org/java_websocket/framing/DataFrame.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; + +/** + * Abstract class to represent data frames + */ +public abstract class DataFrame extends FramedataImpl1 { + + /** + * Class to represent a data frame + * + * @param opcode the opcode to use + */ + public DataFrame(Opcode opcode) { + super(opcode); + } + + @Override + public void isValid() throws InvalidDataException { + //Nothing specific to check + } +} diff --git a/src/main/java/org/java_websocket/framing/Framedata.java b/src/main/java/org/java_websocket/framing/Framedata.java new file mode 100644 index 0000000..d3e8f3c --- /dev/null +++ b/src/main/java/org/java_websocket/framing/Framedata.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import java.nio.ByteBuffer; +import org.java_websocket.enums.Opcode; + +/** + * The interface for the frame + */ +public interface Framedata { + + /** + * Indicates that this is the final fragment in a message. The first fragment MAY also be the + * final fragment. + * + * @return true, if this frame is the final fragment + */ + boolean isFin(); + + /** + * Indicates that this frame has the rsv1 bit set. + * + * @return true, if this frame has the rsv1 bit set + */ + boolean isRSV1(); + + /** + * Indicates that this frame has the rsv2 bit set. + * + * @return true, if this frame has the rsv2 bit set + */ + boolean isRSV2(); + + /** + * Indicates that this frame has the rsv3 bit set. + * + * @return true, if this frame has the rsv3 bit set + */ + boolean isRSV3(); + + /** + * Defines whether the "Payload data" is masked. + * + * @return true, "Payload data" is masked + */ + boolean getTransfereMasked(); + + /** + * Defines the interpretation of the "Payload data". + * + * @return the interpretation as a Opcode + */ + Opcode getOpcode(); + + /** + * The "Payload data" which was sent in this frame + * + * @return the "Payload data" as ByteBuffer + */ + ByteBuffer getPayloadData();// TODO the separation of the application data and the extension data is yet to be done + + /** + * Appends an additional frame to the current frame + *

+ * This methods does not override the opcode, but does override the fin + * + * @param nextframe the additional frame + */ + void append(Framedata nextframe); +} diff --git a/src/main/java/org/java_websocket/framing/FramedataImpl1.java b/src/main/java/org/java_websocket/framing/FramedataImpl1.java new file mode 100644 index 0000000..fc74f7a --- /dev/null +++ b/src/main/java/org/java_websocket/framing/FramedataImpl1.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import java.nio.ByteBuffer; +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.util.ByteBufferUtils; + +/** + * Abstract implementation of a frame + */ +public abstract class FramedataImpl1 implements Framedata { + + /** + * Indicates that this is the final fragment in a message. + */ + private boolean fin; + /** + * Defines the interpretation of the "Payload data". + */ + private Opcode optcode; + + /** + * The unmasked "Payload data" which was sent in this frame + */ + private ByteBuffer unmaskedpayload; + + /** + * Defines whether the "Payload data" is masked. + */ + private boolean transferemasked; + + /** + * Indicates that the rsv1 bit is set or not + */ + private boolean rsv1; + + /** + * Indicates that the rsv2 bit is set or not + */ + private boolean rsv2; + + /** + * Indicates that the rsv3 bit is set or not + */ + private boolean rsv3; + + /** + * Check if the frame is valid due to specification + * + * @throws InvalidDataException thrown if the frame is not a valid frame + */ + public abstract void isValid() throws InvalidDataException; + + /** + * Constructor for a FramedataImpl without any attributes set apart from the opcode + * + * @param op the opcode to use + */ + public FramedataImpl1(Opcode op) { + optcode = op; + unmaskedpayload = ByteBufferUtils.getEmptyByteBuffer(); + fin = true; + transferemasked = false; + rsv1 = false; + rsv2 = false; + rsv3 = false; + } + + @Override + public boolean isRSV1() { + return rsv1; + } + + @Override + public boolean isRSV2() { + return rsv2; + } + + @Override + public boolean isRSV3() { + return rsv3; + } + + @Override + public boolean isFin() { + return fin; + } + + @Override + public Opcode getOpcode() { + return optcode; + } + + @Override + public boolean getTransfereMasked() { + return transferemasked; + } + + @Override + public ByteBuffer getPayloadData() { + return unmaskedpayload; + } + + @Override + public void append(Framedata nextframe) { + ByteBuffer b = nextframe.getPayloadData(); + if (unmaskedpayload == null) { + unmaskedpayload = ByteBuffer.allocate(b.remaining()); + b.mark(); + unmaskedpayload.put(b); + b.reset(); + } else { + b.mark(); + unmaskedpayload.position(unmaskedpayload.limit()); + unmaskedpayload.limit(unmaskedpayload.capacity()); + + if (b.remaining() > unmaskedpayload.remaining()) { + ByteBuffer tmp = ByteBuffer.allocate(b.remaining() + unmaskedpayload.capacity()); + unmaskedpayload.flip(); + tmp.put(unmaskedpayload); + tmp.put(b); + unmaskedpayload = tmp; + + } else { + unmaskedpayload.put(b); + } + unmaskedpayload.rewind(); + b.reset(); + } + fin = nextframe.isFin(); + + } + + @Override + public String toString() { + return "Framedata{ opcode:" + getOpcode() + ", fin:" + isFin() + ", rsv1:" + isRSV1() + + ", rsv2:" + isRSV2() + ", rsv3:" + isRSV3() + ", payload length:[pos:" + unmaskedpayload + .position() + ", len:" + unmaskedpayload.remaining() + "], payload:" + ( + unmaskedpayload.remaining() > 1000 ? "(too big to display)" + : new String(unmaskedpayload.array())) + '}'; + } + + /** + * Set the payload of this frame to the provided payload + * + * @param payload the payload which is to set + */ + public void setPayload(ByteBuffer payload) { + this.unmaskedpayload = payload; + } + + /** + * Set the fin of this frame to the provided boolean + * + * @param fin true if fin has to be set + */ + public void setFin(boolean fin) { + this.fin = fin; + } + + /** + * Set the rsv1 of this frame to the provided boolean + * + * @param rsv1 true if rsv1 has to be set + */ + public void setRSV1(boolean rsv1) { + this.rsv1 = rsv1; + } + + /** + * Set the rsv2 of this frame to the provided boolean + * + * @param rsv2 true if rsv2 has to be set + */ + public void setRSV2(boolean rsv2) { + this.rsv2 = rsv2; + } + + /** + * Set the rsv3 of this frame to the provided boolean + * + * @param rsv3 true if rsv3 has to be set + */ + public void setRSV3(boolean rsv3) { + this.rsv3 = rsv3; + } + + /** + * Set the tranferemask of this frame to the provided boolean + * + * @param transferemasked true if transferemasked has to be set + */ + public void setTransferemasked(boolean transferemasked) { + this.transferemasked = transferemasked; + } + + /** + * Get a frame with a specific opcode + * + * @param opcode the opcode representing the frame + * @return the frame with a specific opcode + */ + public static FramedataImpl1 get(Opcode opcode) { + if (opcode == null) { + throw new IllegalArgumentException("Supplied opcode cannot be null"); + } + switch (opcode) { + case PING: + return new PingFrame(); + case PONG: + return new PongFrame(); + case TEXT: + return new TextFrame(); + case BINARY: + return new BinaryFrame(); + case CLOSING: + return new CloseFrame(); + case CONTINUOUS: + return new ContinuousFrame(); + default: + throw new IllegalArgumentException("Supplied opcode is invalid"); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FramedataImpl1 that = (FramedataImpl1) o; + + if (fin != that.fin) { + return false; + } + if (transferemasked != that.transferemasked) { + return false; + } + if (rsv1 != that.rsv1) { + return false; + } + if (rsv2 != that.rsv2) { + return false; + } + if (rsv3 != that.rsv3) { + return false; + } + if (optcode != that.optcode) { + return false; + } + return unmaskedpayload != null ? unmaskedpayload.equals(that.unmaskedpayload) + : that.unmaskedpayload == null; + } + + @Override + public int hashCode() { + int result = (fin ? 1 : 0); + result = 31 * result + optcode.hashCode(); + result = 31 * result + (unmaskedpayload != null ? unmaskedpayload.hashCode() : 0); + result = 31 * result + (transferemasked ? 1 : 0); + result = 31 * result + (rsv1 ? 1 : 0); + result = 31 * result + (rsv2 ? 1 : 0); + result = 31 * result + (rsv3 ? 1 : 0); + return result; + } +} diff --git a/src/main/java/org/java_websocket/framing/PingFrame.java b/src/main/java/org/java_websocket/framing/PingFrame.java new file mode 100644 index 0000000..ae2b291 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/PingFrame.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; + +/** + * Class to represent a ping frame + */ +public class PingFrame extends ControlFrame { + + /** + * constructor which sets the opcode of this frame to ping + */ + public PingFrame() { + super(Opcode.PING); + } +} diff --git a/src/main/java/org/java_websocket/framing/PongFrame.java b/src/main/java/org/java_websocket/framing/PongFrame.java new file mode 100644 index 0000000..4b58139 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/PongFrame.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; + +/** + * Class to represent a pong frame + */ +public class PongFrame extends ControlFrame { + + /** + * constructor which sets the opcode of this frame to pong + */ + public PongFrame() { + super(Opcode.PONG); + } + + /** + * constructor which sets the opcode of this frame to ping copying over the payload of the ping + * + * @param pingFrame the PingFrame which payload is to copy + */ + public PongFrame(PingFrame pingFrame) { + super(Opcode.PONG); + setPayload(pingFrame.getPayloadData()); + } +} diff --git a/src/main/java/org/java_websocket/framing/TextFrame.java b/src/main/java/org/java_websocket/framing/TextFrame.java new file mode 100644 index 0000000..52154b4 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/TextFrame.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.framing; + +import org.java_websocket.enums.Opcode; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.util.Charsetfunctions; + +/** + * Class to represent a text frames + */ +public class TextFrame extends DataFrame { + + /** + * constructor which sets the opcode of this frame to text + */ + public TextFrame() { + super(Opcode.TEXT); + } + + @Override + public void isValid() throws InvalidDataException { + super.isValid(); + if (!Charsetfunctions.isValidUTF8(getPayloadData())) { + throw new InvalidDataException(CloseFrame.NO_UTF8, "Received text is no valid utf8 string!"); + } + } +} diff --git a/src/main/java/org/java_websocket/framing/package-info.java b/src/main/java/org/java_websocket/framing/package-info.java new file mode 100644 index 0000000..12e1510 --- /dev/null +++ b/src/main/java/org/java_websocket/framing/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all interfaces and implementations in relation with the WebSocket + * frames. + */ +package org.java_websocket.framing; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshake.java b/src/main/java/org/java_websocket/handshake/ClientHandshake.java new file mode 100644 index 0000000..f0cbc3a --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/ClientHandshake.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * The interface for a client handshake + */ +public interface ClientHandshake extends Handshakedata { + + /** + * returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2 + * + * @return the HTTP Request-URI + */ + String getResourceDescriptor(); +} diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java new file mode 100644 index 0000000..8187515 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * The interface for building a handshake for the client + */ +public interface ClientHandshakeBuilder extends HandshakeBuilder, ClientHandshake { + + /** + * Set a specific resource descriptor + * + * @param resourceDescriptor the resource descriptior to set + */ + void setResourceDescriptor(String resourceDescriptor); +} diff --git a/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java new file mode 100644 index 0000000..1f4de20 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/HandshakeBuilder.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * The interface for building a handshake + */ +public interface HandshakeBuilder extends Handshakedata { + + /** + * Setter for the content of the handshake + * + * @param content the content to set + */ + void setContent(byte[] content); + + /** + * Adding a specific field with a specific value + * + * @param name the http field + * @param value the value for this field + */ + void put(String name, String value); +} diff --git a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java new file mode 100644 index 0000000..11ffa43 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * Implementation for a client handshake + */ +public class HandshakeImpl1Client extends HandshakedataImpl1 implements ClientHandshakeBuilder { + + /** + * Attribute for the resource descriptor + */ + private String resourceDescriptor = "*"; + + @Override + public void setResourceDescriptor(String resourceDescriptor) { + if (resourceDescriptor == null) { + throw new IllegalArgumentException("http resource descriptor must not be null"); + } + this.resourceDescriptor = resourceDescriptor; + } + + @Override + public String getResourceDescriptor() { + return resourceDescriptor; + } +} diff --git a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java new file mode 100644 index 0000000..8754014 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Server.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * Implementation for a server handshake + */ +public class HandshakeImpl1Server extends HandshakedataImpl1 implements ServerHandshakeBuilder { + + /** + * Attribute for the http status + */ + private short httpstatus; + + /** + * Attribute for the http status message + */ + private String httpstatusmessage; + + @Override + public String getHttpStatusMessage() { + return httpstatusmessage; + } + + @Override + public short getHttpStatus() { + return httpstatus; + } + + @Override + public void setHttpStatusMessage(String message) { + this.httpstatusmessage = message; + } + + @Override + public void setHttpStatus(short status) { + httpstatus = status; + } +} diff --git a/src/main/java/org/java_websocket/handshake/Handshakedata.java b/src/main/java/org/java_websocket/handshake/Handshakedata.java new file mode 100644 index 0000000..fd270ec --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/Handshakedata.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +import java.util.Iterator; + +/** + * The interface for the data of a handshake + */ +public interface Handshakedata { + + /** + * Iterator for the http fields + * + * @return the http fields + */ + Iterator iterateHttpFields(); + + /** + * Gets the value of the field + * + * @param name The name of the field + * @return the value of the field or an empty String if not in the handshake + */ + String getFieldValue(String name); + + /** + * Checks if this handshake contains a specific field + * + * @param name The name of the field + * @return true, if it contains the field + */ + boolean hasFieldValue(String name); + + /** + * Get the content of the handshake + * + * @return the content as byte-array + */ + byte[] getContent(); +} diff --git a/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java new file mode 100644 index 0000000..62824cd --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +import java.util.Collections; +import java.util.Iterator; +import java.util.TreeMap; + +/** + * Implementation of a handshake builder + */ +public class HandshakedataImpl1 implements HandshakeBuilder { + + /** + * Attribute for the content of the handshake + */ + private byte[] content; + + /** + * Attribute for the http fields and values + */ + private TreeMap map; + + /** + * Constructor for handshake implementation + */ + public HandshakedataImpl1() { + map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + + @Override + public Iterator iterateHttpFields() { + return Collections.unmodifiableSet(map.keySet()).iterator();// Safety first + } + + @Override + public String getFieldValue(String name) { + String s = map.get(name); + if (s == null) { + return ""; + } + return s; + } + + @Override + public byte[] getContent() { + return content; + } + + @Override + public void setContent(byte[] content) { + this.content = content; + } + + @Override + public void put(String name, String value) { + map.put(name, value); + } + + @Override + public boolean hasFieldValue(String name) { + return map.containsKey(name); + } +} diff --git a/src/main/java/org/java_websocket/handshake/ServerHandshake.java b/src/main/java/org/java_websocket/handshake/ServerHandshake.java new file mode 100644 index 0000000..1b5a5a9 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/ServerHandshake.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * Interface for the server handshake + */ +public interface ServerHandshake extends Handshakedata { + + /** + * Get the http status code + * + * @return the http status code + */ + short getHttpStatus(); + + /** + * Get the http status message + * + * @return the http status message + */ + String getHttpStatusMessage(); +} diff --git a/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java new file mode 100644 index 0000000..51212f0 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/ServerHandshakeBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.handshake; + +/** + * The interface for building a handshake for the server + */ +public interface ServerHandshakeBuilder extends HandshakeBuilder, ServerHandshake { + + /** + * Setter for the http status code + * + * @param status the http status code + */ + void setHttpStatus(short status); + + /** + * Setter for the http status message + * + * @param message the http status message + */ + void setHttpStatusMessage(String message); +} diff --git a/src/main/java/org/java_websocket/handshake/package-info.java b/src/main/java/org/java_websocket/handshake/package-info.java new file mode 100644 index 0000000..7af26d4 --- /dev/null +++ b/src/main/java/org/java_websocket/handshake/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all interfaces and implementations in relation with the WebSocket + * handshake. + */ +package org.java_websocket.handshake; + diff --git a/src/main/java/org/java_websocket/interfaces/ISSLChannel.java b/src/main/java/org/java_websocket/interfaces/ISSLChannel.java new file mode 100644 index 0000000..c783c58 --- /dev/null +++ b/src/main/java/org/java_websocket/interfaces/ISSLChannel.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +package org.java_websocket.interfaces; + +import javax.net.ssl.SSLEngine; + +/** + * Interface which specifies all required methods a SSLSocketChannel has to make public. + * + * @since 1.4.1 + */ +public interface ISSLChannel { + + /** + * Get the ssl engine used for the de- and encryption of the communication. + * + * @return the ssl engine of this channel + */ + SSLEngine getSSLEngine(); +} diff --git a/src/main/java/org/java_websocket/interfaces/package-info.java b/src/main/java/org/java_websocket/interfaces/package-info.java new file mode 100644 index 0000000..b70dbc1 --- /dev/null +++ b/src/main/java/org/java_websocket/interfaces/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +/** + * This package encapsulates all new interfaces. + */ +package org.java_websocket.interfaces; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/protocols/IProtocol.java b/src/main/java/org/java_websocket/protocols/IProtocol.java new file mode 100644 index 0000000..5300aed --- /dev/null +++ b/src/main/java/org/java_websocket/protocols/IProtocol.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.protocols; + +/** + * Interface which specifies all required methods for a Sec-WebSocket-Protocol + * + * @since 1.3.7 + */ +public interface IProtocol { + + /** + * Check if the received Sec-WebSocket-Protocol header field contains a offer for the specific + * protocol + * + * @param inputProtocolHeader the received Sec-WebSocket-Protocol header field offered by the + * other endpoint + * @return true, if the offer does fit to this specific protocol + * @since 1.3.7 + */ + boolean acceptProvidedProtocol(String inputProtocolHeader); + + /** + * Return the specific Sec-WebSocket-protocol header offer for this protocol if the endpoint. If + * the extension returns an empty string (""), the offer will not be included in the handshake. + * + * @return the specific Sec-WebSocket-Protocol header for this protocol + * @since 1.3.7 + */ + String getProvidedProtocol(); + + /** + * To prevent protocols to be used more than once the Websocket implementation should call this + * method in order to create a new usable version of a given protocol instance. + * + * @return a copy of the protocol + * @since 1.3.7 + */ + IProtocol copyInstance(); + + /** + * Return a string which should contain the protocol name as well as additional information about + * the current configurations for this protocol (DEBUG purposes) + * + * @return a string containing the protocol name as well as additional information + * @since 1.3.7 + */ + String toString(); +} diff --git a/src/main/java/org/java_websocket/protocols/Protocol.java b/src/main/java/org/java_websocket/protocols/Protocol.java new file mode 100644 index 0000000..f7fbfb5 --- /dev/null +++ b/src/main/java/org/java_websocket/protocols/Protocol.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.protocols; + +import java.util.regex.Pattern; + +/** + * Class which represents the protocol used as Sec-WebSocket-Protocol + * + * @since 1.3.7 + */ +public class Protocol implements IProtocol { + + private static final Pattern patternSpace = Pattern.compile(" "); + private static final Pattern patternComma = Pattern.compile(","); + + /** + * Attribute for the provided protocol + */ + private final String providedProtocol; + + /** + * Constructor for a Sec-Websocket-Protocol + * + * @param providedProtocol the protocol string + */ + public Protocol(String providedProtocol) { + if (providedProtocol == null) { + throw new IllegalArgumentException(); + } + this.providedProtocol = providedProtocol; + } + + @Override + public boolean acceptProvidedProtocol(String inputProtocolHeader) { + if ("".equals(providedProtocol)) { + return true; + } + String protocolHeader = patternSpace.matcher(inputProtocolHeader).replaceAll(""); + String[] headers = patternComma.split(protocolHeader); + for (String header : headers) { + if (providedProtocol.equals(header)) { + return true; + } + } + return false; + } + + @Override + public String getProvidedProtocol() { + return this.providedProtocol; + } + + @Override + public IProtocol copyInstance() { + return new Protocol(getProvidedProtocol()); + } + + @Override + public String toString() { + return getProvidedProtocol(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Protocol protocol = (Protocol) o; + + return providedProtocol.equals(protocol.providedProtocol); + } + + @Override + public int hashCode() { + return providedProtocol.hashCode(); + } +} diff --git a/src/main/java/org/java_websocket/protocols/package-info.java b/src/main/java/org/java_websocket/protocols/package-info.java new file mode 100644 index 0000000..6f8132b --- /dev/null +++ b/src/main/java/org/java_websocket/protocols/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all interfaces and implementations in relation with the WebSocket + * Sec-WebSocket-Protocol. + */ +package org.java_websocket.protocols; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java new file mode 100644 index 0000000..1246b22 --- /dev/null +++ b/src/main/java/org/java_websocket/server/CustomSSLWebSocketServerFactory.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.server; + +import java.io.IOException; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import org.java_websocket.SSLSocketChannel2; + +/** + * WebSocketFactory that can be configured to only support specific protocols and cipher suites. + */ +public class CustomSSLWebSocketServerFactory extends DefaultSSLWebSocketServerFactory { + + /** + * The enabled protocols saved as a String array + */ + private final String[] enabledProtocols; + + /** + * The enabled ciphersuites saved as a String array + */ + private final String[] enabledCiphersuites; + + /** + * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher + * suites. + * + * @param sslContext - can not be null + * @param enabledProtocols - only these protocols are enabled, when null default + * settings will be used. + * @param enabledCiphersuites - only these cipher suites are enabled, when null + * default settings will be used. + */ + public CustomSSLWebSocketServerFactory(SSLContext sslContext, String[] enabledProtocols, + String[] enabledCiphersuites) { + this(sslContext, Executors.newSingleThreadScheduledExecutor(), enabledProtocols, + enabledCiphersuites); + } + + /** + * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher + * suites. + * + * @param sslContext - can not be null + * @param executerService - can not be null + * @param enabledProtocols - only these protocols are enabled, when null default + * settings will be used. + * @param enabledCiphersuites - only these cipher suites are enabled, when null + * default settings will be used. + */ + public CustomSSLWebSocketServerFactory(SSLContext sslContext, ExecutorService executerService, + String[] enabledProtocols, String[] enabledCiphersuites) { + super(sslContext, executerService); + this.enabledProtocols = enabledProtocols; + this.enabledCiphersuites = enabledCiphersuites; + } + + @Override + public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException { + SSLEngine e = sslcontext.createSSLEngine(); + if (enabledProtocols != null) { + e.setEnabledProtocols(enabledProtocols); + } + if (enabledCiphersuites != null) { + e.setEnabledCipherSuites(enabledCiphersuites); + } + e.setUseClientMode(false); + return new SSLSocketChannel2(channel, e, exec, key); + } + +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java new file mode 100644 index 0000000..f273203 --- /dev/null +++ b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.server; + +import java.io.IOException; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import org.java_websocket.SSLSocketChannel2; +import org.java_websocket.WebSocketAdapter; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.WebSocketServerFactory; +import org.java_websocket.drafts.Draft; + +public class DefaultSSLWebSocketServerFactory implements WebSocketServerFactory { + + protected SSLContext sslcontext; + protected ExecutorService exec; + + public DefaultSSLWebSocketServerFactory(SSLContext sslContext) { + this(sslContext, Executors.newSingleThreadScheduledExecutor()); + } + + public DefaultSSLWebSocketServerFactory(SSLContext sslContext, ExecutorService exec) { + if (sslContext == null || exec == null) { + throw new IllegalArgumentException(); + } + this.sslcontext = sslContext; + this.exec = exec; + } + + @Override + public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException { + SSLEngine e = sslcontext.createSSLEngine(); + /* + * See https://github.com/TooTallNate/Java-WebSocket/issues/466 + * + * We remove TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 from the enabled ciphers since it is just available when you patch your java installation directly. + * E.g. firefox requests this cipher and this causes some dcs/instable connections + */ + List ciphers = new ArrayList<>(Arrays.asList(e.getEnabledCipherSuites())); + ciphers.remove("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + e.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()])); + e.setUseClientMode(false); + return new SSLSocketChannel2(channel, e, exec, key); + } + + @Override + public WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d) { + return new WebSocketImpl(a, d); + } + + @Override + public WebSocketImpl createWebSocket(WebSocketAdapter a, List d) { + return new WebSocketImpl(a, d); + } + + @Override + public void close() { + exec.shutdown(); + } +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java new file mode 100644 index 0000000..80527e8 --- /dev/null +++ b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.server; + +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.List; +import org.java_websocket.WebSocketAdapter; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.WebSocketServerFactory; +import org.java_websocket.drafts.Draft; + +public class DefaultWebSocketServerFactory implements WebSocketServerFactory { + + @Override + public WebSocketImpl createWebSocket(WebSocketAdapter a, Draft d) { + return new WebSocketImpl(a, d); + } + + @Override + public WebSocketImpl createWebSocket(WebSocketAdapter a, List d) { + return new WebSocketImpl(a, d); + } + + @Override + public SocketChannel wrapChannel(SocketChannel channel, SelectionKey key) { + return channel; + } + + @Override + public void close() { + //Nothing to do for a normal ws factory + } +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java new file mode 100644 index 0000000..d7685bf --- /dev/null +++ b/src/main/java/org/java_websocket/server/SSLParametersWebSocketServerFactory.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.server; + +import java.io.IOException; +import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import org.java_websocket.SSLSocketChannel2; + +/** + * WebSocketFactory that can be configured to only support specific protocols and cipher suites. + */ +public class SSLParametersWebSocketServerFactory extends DefaultSSLWebSocketServerFactory { + + private final SSLParameters sslParameters; + + /** + * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher + * suites. + * + * @param sslContext - can not be null + * @param sslParameters - can not be null + */ + public SSLParametersWebSocketServerFactory(SSLContext sslContext, SSLParameters sslParameters) { + this(sslContext, Executors.newSingleThreadScheduledExecutor(), sslParameters); + } + + /** + * New CustomSSLWebSocketServerFactory configured to only support given protocols and given cipher + * suites. + * + * @param sslContext - can not be null + * @param executerService - can not be null + * @param sslParameters - can not be null + */ + public SSLParametersWebSocketServerFactory(SSLContext sslContext, ExecutorService executerService, + SSLParameters sslParameters) { + super(sslContext, executerService); + if (sslParameters == null) { + throw new IllegalArgumentException(); + } + this.sslParameters = sslParameters; + } + + @Override + public ByteChannel wrapChannel(SocketChannel channel, SelectionKey key) throws IOException { + SSLEngine e = sslcontext.createSSLEngine(); + e.setUseClientMode(false); + e.setSSLParameters(sslParameters); + return new SSLSocketChannel2(channel, e, exec, key); + } +} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/WebSocketServer.java b/src/main/java/org/java_websocket/server/WebSocketServer.java new file mode 100644 index 0000000..d5be3a4 --- /dev/null +++ b/src/main/java/org/java_websocket/server/WebSocketServer.java @@ -0,0 +1,1171 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.server; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.java_websocket.AbstractWebSocket; +import org.java_websocket.SocketChannelIOHelper; +import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketFactory; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.WebSocketServerFactory; +import org.java_websocket.WrappedByteChannel; +import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.java_websocket.exceptions.WrappedIOException; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.Handshakedata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * WebSocketServer is an abstract class that only takes care of the + * HTTP handshake portion of WebSockets. It's up to a subclass to add functionality/purpose to the + * server. + */ +public abstract class WebSocketServer extends AbstractWebSocket implements Runnable { + + private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors(); + + /** + * Logger instance + * + * @since 1.4.0 + */ + private final Logger log = LoggerFactory.getLogger(WebSocketServer.class); + + /** + * Holds the list of active WebSocket connections. "Active" means WebSocket handshake is complete + * and socket can be written to, or read from. + */ + private final Collection connections; + /** + * The port number that this WebSocket server should listen on. Default is + * WebSocketImpl.DEFAULT_PORT. + */ + private final InetSocketAddress address; + /** + * The socket channel for this WebSocket server. + */ + private ServerSocketChannel server; + /** + * The 'Selector' used to get event keys from the underlying socket. + */ + private Selector selector; + /** + * The Draft of the WebSocket protocol the Server is adhering to. + */ + private List drafts; + + private Thread selectorthread; + + private final AtomicBoolean isclosed = new AtomicBoolean(false); + + protected List decoders; + + private List iqueue; + private BlockingQueue buffers; + private int queueinvokes = 0; + private final AtomicInteger queuesize = new AtomicInteger(0); + + private WebSocketServerFactory wsf = new DefaultWebSocketServerFactory(); + + /** + * Attribute which allows you to configure the socket "backlog" parameter which determines how + * many client connections can be queued. + * + * @since 1.5.0 + */ + private int maxPendingConnections = -1; + + /** + * Creates a WebSocketServer that will attempt to listen on port WebSocketImpl.DEFAULT_PORT. + * + * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here + */ + public WebSocketServer() { + this(new InetSocketAddress(WebSocketImpl.DEFAULT_PORT), AVAILABLE_PROCESSORS, null); + } + + /** + * Creates a WebSocketServer that will attempt to bind/listen on the given address. + * + * @param address The address to listen to + * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here + */ + public WebSocketServer(InetSocketAddress address) { + this(address, AVAILABLE_PROCESSORS, null); + } + + /** + * @param address The address (host:port) this server should listen on. + * incoming network data. By default this will be Runtime.getRuntime().availableProcessors() + * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here + */ + public WebSocketServer(InetSocketAddress address, int decodercount) { + this(address, decodercount, null); + } + + /** + * @param address The address (host:port) this server should listen on. + * @param drafts The versions of the WebSocket protocol that this server instance should comply + * to. Clients that use an other protocol version will be rejected. + * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here + */ + public WebSocketServer(InetSocketAddress address, List drafts) { + this(address, AVAILABLE_PROCESSORS, drafts); + } + + /** + * @param address The address (host:port) this server should listen on. + * incoming network data. By default this will be Runtime.getRuntime().availableProcessors() + * @param drafts The versions of the WebSocket protocol that this server instance should + * comply to. Clients that use an other protocol version will be rejected. + * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here + */ + public WebSocketServer(InetSocketAddress address, int decodercount, List drafts) { + this(address, decodercount, drafts, new HashSet()); + } + + // Small internal helper function to get around limitations of Java constructors. + private static InetSocketAddress checkAddressOfExistingChannel(ServerSocketChannel existingChannel) { + assert existingChannel.isOpen(); + SocketAddress addr; + try { + addr = existingChannel.getLocalAddress(); + } catch (IOException e) { + throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound", e); + } + if (addr == null) { + throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound"); + } + return (InetSocketAddress)addr; + } + + /** + * @param existingChannel An already open and bound server socket channel, which this server will use. + * For example, it can be System.inheritedChannel() to implement socket activation. + */ + public WebSocketServer(ServerSocketChannel existingChannel) { + this(checkAddressOfExistingChannel(existingChannel)); + this.server = existingChannel; + } + + /** + * Creates a WebSocketServer that will attempt to bind/listen on the given address, and + * comply with Draft version draft. + * + * @param address The address (host:port) this server should listen on. + * @param decodercount The number of {@link WebSocketWorker}s that will be used to process + * the incoming network data. By default this will be + * Runtime.getRuntime().availableProcessors() + * @param drafts The versions of the WebSocket protocol that this server instance + * should comply to. Clients that use an other protocol version will + * be rejected. + * @param connectionscontainer Allows to specify a collection that will be used to store the + * websockets in.
If you plan to often iterate through the + * currently connected websockets you may want to use a collection + * that does not require synchronization like a {@link + * CopyOnWriteArraySet}. In that case make sure that you overload + * {@link #removeConnection(WebSocket)} and {@link + * #addConnection(WebSocket)}.
By default a {@link HashSet} will + * be used. + * @see #removeConnection(WebSocket) for more control over syncronized operation + * @see more about + * drafts + */ + public WebSocketServer(InetSocketAddress address, int decodercount, List drafts, + Collection connectionscontainer) { + if (address == null || decodercount < 1 || connectionscontainer == null) { + throw new IllegalArgumentException( + "address and connectionscontainer must not be null and you need at least 1 decoder"); + } + + if (drafts == null) { + this.drafts = Collections.emptyList(); + } else { + this.drafts = drafts; + } + + this.address = address; + this.connections = connectionscontainer; + setTcpNoDelay(false); + setReuseAddr(false); + iqueue = new LinkedList<>(); + + decoders = new ArrayList<>(decodercount); + buffers = new LinkedBlockingQueue<>(); + for (int i = 0; i < decodercount; i++) { + WebSocketWorker ex = new WebSocketWorker(); + decoders.add(ex); + } + } + + + /** + * Starts the server selectorthread that binds to the currently set port number and listeners for + * WebSocket connection requests. Creates a fixed thread pool with the size {@link + * WebSocketServer#AVAILABLE_PROCESSORS}
May only be called once. + *

+ * Alternatively you can call {@link WebSocketServer#run()} directly. + * + * @throws IllegalStateException Starting an instance again + */ + public void start() { + if (selectorthread != null) { + throw new IllegalStateException(getClass().getName() + " can only be started once."); + } + Thread t = new Thread(this); + t.setDaemon(isDaemon()); + t.start(); + } + + public void stop(int timeout) throws InterruptedException { + stop(timeout, ""); + } + + /** + * Closes all connected clients sockets, then closes the underlying ServerSocketChannel, + * effectively killing the server socket selectorthread, freeing the port the server was bound to + * and stops all internal workerthreads. + *

+ * If this method is called before the server is started it will never start. + * + * @param timeout Specifies how many milliseconds the overall close handshaking may take + * altogether before the connections are closed without proper close + * handshaking. + * @param closeMessage Specifies message for remote client
+ * @throws InterruptedException Interrupt + */ + public void stop(int timeout, String closeMessage) throws InterruptedException { + if (!isclosed.compareAndSet(false, + true)) { // this also makes sure that no further connections will be added to this.connections + return; + } + + List socketsToClose; + + // copy the connections in a list (prevent callback deadlocks) + synchronized (connections) { + socketsToClose = new ArrayList<>(connections); + } + + for (WebSocket ws : socketsToClose) { + ws.close(CloseFrame.GOING_AWAY, closeMessage); + } + + wsf.close(); + + synchronized (this) { + if (selectorthread != null && selector != null) { + selector.wakeup(); + selectorthread.join(timeout); + } + } + } + + public void stop() throws InterruptedException { + stop(0); + } + + /** + * Returns all currently connected clients. This collection does not allow any modification e.g. + * removing a client. + * + * @return A unmodifiable collection of all currently connected clients + * @since 1.3.8 + */ + public Collection getConnections() { + synchronized (connections) { + return Collections.unmodifiableCollection(new ArrayList<>(connections)); + } + } + + public InetSocketAddress getAddress() { + return this.address; + } + + /** + * Gets the port number that this server listens on. + * + * @return The port number. + */ + public int getPort() { + int port = getAddress().getPort(); + if (port == 0 && server != null) { + port = server.socket().getLocalPort(); + } + return port; + } + + @Override + public void setDaemon(boolean daemon) { + // pass it to the AbstractWebSocket too, to use it on the connectionLostChecker thread factory + super.setDaemon(daemon); + // we need to apply this to the decoders as well since they were created during the constructor + for (WebSocketWorker w : decoders) { + if (w.isAlive()) { + throw new IllegalStateException("Cannot call setDaemon after server is already started!"); + } else { + w.setDaemon(daemon); + } + } + } + + /** + * Get the list of active drafts + * + * @return the available drafts for this server + */ + public List getDraft() { + return Collections.unmodifiableList(drafts); + } + + /** + * Set the requested maximum number of pending connections on the socket. The exact semantics are + * implementation specific. The value provided should be greater than 0. If it is less than or + * equal to 0, then an implementation specific default will be used. This option will be passed as + * "backlog" parameter to {@link ServerSocket#bind(SocketAddress, int)} + * + * @since 1.5.0 + * @param numberOfConnections the new number of allowed pending connections + */ + public void setMaxPendingConnections(int numberOfConnections) { + maxPendingConnections = numberOfConnections; + } + + /** + * Returns the currently configured maximum number of pending connections. + * + * @see #setMaxPendingConnections(int) + * @since 1.5.0 + * @return the maximum number of pending connections + */ + public int getMaxPendingConnections() { + return maxPendingConnections; + } + + // Runnable IMPLEMENTATION ///////////////////////////////////////////////// + public void run() { + if (!doEnsureSingleThread()) { + return; + } + if (!doSetupSelectorAndServerThread()) { + return; + } + try { + int shutdownCount = 5; + int selectTimeout = 0; + while (!selectorthread.isInterrupted() && shutdownCount != 0) { + SelectionKey key = null; + try { + if (isclosed.get()) { + selectTimeout = 5; + } + int keyCount = selector.select(selectTimeout); + if (keyCount == 0 && isclosed.get()) { + shutdownCount--; + } + Set keys = selector.selectedKeys(); + Iterator i = keys.iterator(); + + while (i.hasNext()) { + key = i.next(); + + if (!key.isValid()) { + continue; + } + + if (key.isAcceptable()) { + doAccept(key, i); + continue; + } + + if (key.isReadable() && !doRead(key, i)) { + continue; + } + + if (key.isWritable()) { + doWrite(key); + } + } + doAdditionalRead(); + } catch (CancelledKeyException e) { + // an other thread may cancel the key + } catch (ClosedByInterruptException e) { + return; // do the same stuff as when InterruptedException is thrown + } catch (WrappedIOException ex) { + handleIOException(key, ex.getConnection(), ex.getIOException()); + } catch (IOException ex) { + handleIOException(key, null, ex); + } catch (InterruptedException e) { + // FIXME controlled shutdown (e.g. take care of buffermanagement) + Thread.currentThread().interrupt(); + } + } + } catch (RuntimeException e) { + // should hopefully never occur + handleFatal(null, e); + } finally { + doServerShutdown(); + } + } + + /** + * Do an additional read + * + * @throws InterruptedException thrown by taking a buffer + * @throws IOException if an error happened during read + */ + private void doAdditionalRead() throws InterruptedException, IOException { + WebSocketImpl conn; + while (!iqueue.isEmpty()) { + conn = iqueue.remove(0); + WrappedByteChannel c = ((WrappedByteChannel) conn.getChannel()); + ByteBuffer buf = takeBuffer(); + try { + if (SocketChannelIOHelper.readMore(buf, conn, c)) { + iqueue.add(conn); + } + if (buf.hasRemaining()) { + conn.inQueue.put(buf); + queue(conn); + } else { + pushBuffer(buf); + } + } catch (IOException e) { + pushBuffer(buf); + throw e; + } + } + } + + /** + * Execute a accept operation + * + * @param key the selectionkey to read off + * @param i the iterator for the selection keys + * @throws InterruptedException thrown by taking a buffer + * @throws IOException if an error happened during accept + */ + private void doAccept(SelectionKey key, Iterator i) + throws IOException, InterruptedException { + if (!onConnect(key)) { + key.cancel(); + return; + } + + SocketChannel channel = server.accept(); + if (channel == null) { + return; + } + channel.configureBlocking(false); + Socket socket = channel.socket(); + socket.setTcpNoDelay(isTcpNoDelay()); + socket.setKeepAlive(true); + WebSocketImpl w = wsf.createWebSocket(this, drafts); + w.setSelectionKey(channel.register(selector, SelectionKey.OP_READ, w)); + try { + w.setChannel(wsf.wrapChannel(channel, w.getSelectionKey())); + i.remove(); + allocateBuffers(w); + } catch (IOException ex) { + if (w.getSelectionKey() != null) { + w.getSelectionKey().cancel(); + } + + handleIOException(w.getSelectionKey(), null, ex); + } + } + + /** + * Execute a read operation + * + * @param key the selectionkey to read off + * @param i the iterator for the selection keys + * @return true, if the read was successful, or false if there was an error + * @throws InterruptedException thrown by taking a buffer + * @throws IOException if an error happened during read + */ + private boolean doRead(SelectionKey key, Iterator i) + throws InterruptedException, WrappedIOException { + WebSocketImpl conn = (WebSocketImpl) key.attachment(); + ByteBuffer buf = takeBuffer(); + if (conn.getChannel() == null) { + key.cancel(); + + handleIOException(key, conn, new IOException()); + return false; + } + try { + if (SocketChannelIOHelper.read(buf, conn, conn.getChannel())) { + if (buf.hasRemaining()) { + conn.inQueue.put(buf); + queue(conn); + i.remove(); + if (conn.getChannel() instanceof WrappedByteChannel && ((WrappedByteChannel) conn + .getChannel()).isNeedRead()) { + iqueue.add(conn); + } + } else { + pushBuffer(buf); + } + } else { + pushBuffer(buf); + } + } catch (IOException e) { + pushBuffer(buf); + throw new WrappedIOException(conn, e); + } + return true; + } + + /** + * Execute a write operation + * + * @param key the selectionkey to write on + * @throws IOException if an error happened during batch + */ + private void doWrite(SelectionKey key) throws WrappedIOException { + WebSocketImpl conn = (WebSocketImpl) key.attachment(); + try { + if (SocketChannelIOHelper.batch(conn, conn.getChannel()) && key.isValid()) { + key.interestOps(SelectionKey.OP_READ); + } + } catch (IOException e) { + throw new WrappedIOException(conn, e); + } + } + + /** + * Setup the selector thread as well as basic server settings + * + * @return true, if everything was successful, false if some error happened + */ + private boolean doSetupSelectorAndServerThread() { + selectorthread.setName("WebSocketSelector-" + selectorthread.getId()); + try { + if (server == null) { + server = ServerSocketChannel.open(); + // If 'server' is not null, that means WebSocketServer was created from existing channel. + } + server.configureBlocking(false); + ServerSocket socket = server.socket(); + int receiveBufferSize = getReceiveBufferSize(); + if (receiveBufferSize > 0) { + socket.setReceiveBufferSize(receiveBufferSize); + } + socket.setReuseAddress(isReuseAddr()); + // Socket may be already bound, if an existing channel was passed to constructor. + // In this case we cannot modify backlog size from pure Java code, so leave it as is. + if (!socket.isBound()) { + socket.bind(address, getMaxPendingConnections()); + } + selector = Selector.open(); + server.register(selector, server.validOps()); + startConnectionLostTimer(); + for (WebSocketWorker ex : decoders) { + ex.start(); + } + onStart(); + } catch (IOException ex) { + handleFatal(null, ex); + return false; + } + return true; + } + + /** + * The websocket server can only be started once + * + * @return true, if the server can be started, false if already a thread is running + */ + private boolean doEnsureSingleThread() { + synchronized (this) { + if (selectorthread != null) { + throw new IllegalStateException(getClass().getName() + " can only be started once."); + } + selectorthread = Thread.currentThread(); + if (isclosed.get()) { + return false; + } + } + return true; + } + + /** + * Clean up everything after a shutdown + */ + private void doServerShutdown() { + stopConnectionLostTimer(); + if (decoders != null) { + for (WebSocketWorker w : decoders) { + w.interrupt(); + } + } + if (selector != null) { + try { + selector.close(); + } catch (IOException e) { + log.error("IOException during selector.close", e); + onError(null, e); + } + } + if (server != null) { + try { + server.close(); + } catch (IOException e) { + log.error("IOException during server.close", e); + onError(null, e); + } + } + } + + protected void allocateBuffers(WebSocket c) throws InterruptedException { + if (queuesize.get() >= 2 * decoders.size() + 1) { + return; + } + queuesize.incrementAndGet(); + buffers.put(createBuffer()); + } + + protected void releaseBuffers(WebSocket c) throws InterruptedException { + // queuesize.decrementAndGet(); + // takeBuffer(); + } + + public ByteBuffer createBuffer() { + int receiveBufferSize = getReceiveBufferSize(); + return ByteBuffer.allocate(receiveBufferSize > 0 ? receiveBufferSize : DEFAULT_READ_BUFFER_SIZE); + } + + protected void queue(WebSocketImpl ws) throws InterruptedException { + if (ws.getWorkerThread() == null) { + ws.setWorkerThread(decoders.get(queueinvokes % decoders.size())); + queueinvokes++; + } + ws.getWorkerThread().put(ws); + } + + private ByteBuffer takeBuffer() throws InterruptedException { + return buffers.take(); + } + + private void pushBuffer(ByteBuffer buf) throws InterruptedException { + if (buffers.size() > queuesize.intValue()) { + return; + } + buffers.put(buf); + } + + private void handleIOException(SelectionKey key, WebSocket conn, IOException ex) { + // onWebsocketError( conn, ex );// conn may be null here + if (key != null) { + key.cancel(); + } + if (conn != null) { + conn.closeConnection(CloseFrame.ABNORMAL_CLOSE, ex.getMessage()); + } else if (key != null) { + SelectableChannel channel = key.channel(); + if (channel != null && channel + .isOpen()) { // this could be the case if the IOException ex is a SSLException + try { + channel.close(); + } catch (IOException e) { + // there is nothing that must be done here + } + log.trace("Connection closed because of exception", ex); + } + } + } + + private void handleFatal(WebSocket conn, Exception e) { + log.error("Shutdown due to fatal error", e); + onError(conn, e); + + String causeMessage = e.getCause() != null ? " caused by " + e.getCause().getClass().getName() : ""; + String errorMessage = "Got error on server side: " + e.getClass().getName() + causeMessage; + try { + stop(0, errorMessage); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + log.error("Interrupt during stop", e); + onError(null, e1); + } + + //Shutting down WebSocketWorkers, see #222 + if (decoders != null) { + for (WebSocketWorker w : decoders) { + w.interrupt(); + } + } + if (selectorthread != null) { + selectorthread.interrupt(); + } + } + + @Override + public final void onWebsocketMessage(WebSocket conn, String message) { + onMessage(conn, message); + } + + + @Override + public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) { + onMessage(conn, blob); + } + + @Override + public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) { + if (addConnection(conn)) { + onOpen(conn, (ClientHandshake) handshake); + } + } + + @Override + public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) { + selector.wakeup(); + try { + if (removeConnection(conn)) { + onClose(conn, code, reason, remote); + } + } finally { + try { + releaseBuffers(conn); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + } + + /** + * This method performs remove operations on the connection and therefore also gives control over + * whether the operation shall be synchronized + *

+ * {@link #WebSocketServer(InetSocketAddress, int, List, Collection)} allows to specify a + * collection which will be used to store current connections in.
Depending on the type on the + * connection, modifications of that collection may have to be synchronized. + * + * @param ws The Websocket connection which should be removed + * @return Removing connection successful + */ + protected boolean removeConnection(WebSocket ws) { + boolean removed = false; + synchronized (connections) { + if (this.connections.contains(ws)) { + removed = this.connections.remove(ws); + } else { + //Don't throw an assert error if the ws is not in the list. e.g. when the other endpoint did not send any handshake. see #512 + log.trace( + "Removing connection which is not in the connections collection! Possible no handshake received! {}", + ws); + } + } + if (isclosed.get() && connections.isEmpty()) { + selectorthread.interrupt(); + } + return removed; + } + + /** + * @param ws the Websocket connection which should be added + * @return Adding connection successful + * @see #removeConnection(WebSocket) + */ + protected boolean addConnection(WebSocket ws) { + if (!isclosed.get()) { + synchronized (connections) { + return this.connections.add(ws); + } + } else { + // This case will happen when a new connection gets ready while the server is already stopping. + ws.close(CloseFrame.GOING_AWAY); + return true;// for consistency sake we will make sure that both onOpen will be called + } + } + + @Override + public final void onWebsocketError(WebSocket conn, Exception ex) { + onError(conn, ex); + } + + @Override + public final void onWriteDemand(WebSocket w) { + WebSocketImpl conn = (WebSocketImpl) w; + try { + conn.getSelectionKey().interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); + } catch (CancelledKeyException e) { + // the thread which cancels key is responsible for possible cleanup + conn.outQueue.clear(); + } + selector.wakeup(); + } + + @Override + public void onWebsocketCloseInitiated(WebSocket conn, int code, String reason) { + onCloseInitiated(conn, code, reason); + } + + @Override + public void onWebsocketClosing(WebSocket conn, int code, String reason, boolean remote) { + onClosing(conn, code, reason, remote); + + } + + public void onCloseInitiated(WebSocket conn, int code, String reason) { + } + + public void onClosing(WebSocket conn, int code, String reason, boolean remote) { + + } + + public final void setWebSocketFactory(WebSocketServerFactory wsf) { + if (this.wsf != null) { + this.wsf.close(); + } + this.wsf = wsf; + } + + public final WebSocketFactory getWebSocketFactory() { + return wsf; + } + + /** + * Returns whether a new connection shall be accepted or not.
Therefore method is well suited + * to implement some kind of connection limitation.
+ * + * @param key the SelectionKey for the new connection + * @return Can this new connection be accepted + * @see #onOpen(WebSocket, ClientHandshake) + * @see #onWebsocketHandshakeReceivedAsServer(WebSocket, Draft, ClientHandshake) + **/ + protected boolean onConnect(SelectionKey key) { + return true; + } + + /** + * Getter to return the socket used by this specific connection + * + * @param conn The specific connection + * @return The socket used by this connection + */ + private Socket getSocket(WebSocket conn) { + WebSocketImpl impl = (WebSocketImpl) conn; + return ((SocketChannel) impl.getSelectionKey().channel()).socket(); + } + + @Override + public InetSocketAddress getLocalSocketAddress(WebSocket conn) { + return (InetSocketAddress) getSocket(conn).getLocalSocketAddress(); + } + + @Override + public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { + return (InetSocketAddress) getSocket(conn).getRemoteSocketAddress(); + } + + /** + * Called after an opening handshake has been performed and the given websocket is ready to be + * written on. + * + * @param conn The WebSocket instance this event is occurring on. + * @param handshake The handshake of the websocket instance + */ + public abstract void onOpen(WebSocket conn, ClientHandshake handshake); + + /** + * Called after the websocket connection has been closed. + * + * @param conn The WebSocket instance this event is occurring on. + * @param code The codes can be looked up here: {@link CloseFrame} + * @param reason Additional information string + * @param remote Returns whether or not the closing of the connection was initiated by the remote + * host. + **/ + public abstract void onClose(WebSocket conn, int code, String reason, boolean remote); + + /** + * Callback for string messages received from the remote host + * + * @param conn The WebSocket instance this event is occurring on. + * @param message The UTF-8 decoded message that was received. + * @see #onMessage(WebSocket, ByteBuffer) + **/ + public abstract void onMessage(WebSocket conn, String message); + + /** + * Called when errors occurs. If an error causes the websocket connection to fail {@link + * #onClose(WebSocket, int, String, boolean)} will be called additionally.
This method will be + * called primarily because of IO or protocol errors.
If the given exception is an + * RuntimeException that probably means that you encountered a bug.
+ * + * @param conn Can be null if there error does not belong to one specific websocket. For example + * if the servers port could not be bound. + * @param ex The exception causing this error + **/ + public abstract void onError(WebSocket conn, Exception ex); + + /** + * Called when the server started up successfully. + *

+ * If any error occurred, onError is called instead. + */ + public abstract void onStart(); + + /** + * Callback for binary messages received from the remote host + * + * @param conn The WebSocket instance this event is occurring on. + * @param message The binary message that was received. + * @see #onMessage(WebSocket, ByteBuffer) + **/ + public void onMessage(WebSocket conn, ByteBuffer message) { + } + + /** + * Send a text to all connected endpoints + * + * @param text the text to send to the endpoints + */ + public void broadcast(String text) { + broadcast(text, connections); + } + + /** + * Send a byte array to all connected endpoints + * + * @param data the data to send to the endpoints + */ + public void broadcast(byte[] data) { + broadcast(data, connections); + } + + /** + * Send a ByteBuffer to all connected endpoints + * + * @param data the data to send to the endpoints + */ + public void broadcast(ByteBuffer data) { + broadcast(data, connections); + } + + /** + * Send a byte array to a specific collection of websocket connections + * + * @param data the data to send to the endpoints + * @param clients a collection of endpoints to whom the text has to be send + */ + public void broadcast(byte[] data, Collection clients) { + if (data == null || clients == null) { + throw new IllegalArgumentException(); + } + broadcast(ByteBuffer.wrap(data), clients); + } + + /** + * Send a ByteBuffer to a specific collection of websocket connections + * + * @param data the data to send to the endpoints + * @param clients a collection of endpoints to whom the text has to be send + */ + public void broadcast(ByteBuffer data, Collection clients) { + if (data == null || clients == null) { + throw new IllegalArgumentException(); + } + doBroadcast(data, clients); + } + + /** + * Send a text to a specific collection of websocket connections + * + * @param text the text to send to the endpoints + * @param clients a collection of endpoints to whom the text has to be send + */ + public void broadcast(String text, Collection clients) { + if (text == null || clients == null) { + throw new IllegalArgumentException(); + } + doBroadcast(text, clients); + } + + /** + * Private method to cache all the frames to improve memory footprint and conversion time + * + * @param data the data to broadcast + * @param clients the clients to send the message to + */ + private void doBroadcast(Object data, Collection clients) { + String strData = null; + if (data instanceof String) { + strData = (String) data; + } + ByteBuffer byteData = null; + if (data instanceof ByteBuffer) { + byteData = (ByteBuffer) data; + } + if (strData == null && byteData == null) { + return; + } + Map> draftFrames = new HashMap<>(); + List clientCopy; + synchronized (clients) { + clientCopy = new ArrayList<>(clients); + } + for (WebSocket client : clientCopy) { + if (client != null) { + Draft draft = client.getDraft(); + fillFrames(draft, draftFrames, strData, byteData); + try { + client.sendFrame(draftFrames.get(draft)); + } catch (WebsocketNotConnectedException e) { + //Ignore this exception in this case + } + } + } + } + + /** + * Fills the draftFrames with new data for the broadcast + * + * @param draft The draft to use + * @param draftFrames The list of frames per draft to fill + * @param strData the string data, can be null + * @param byteData the byte buffer data, can be null + */ + private void fillFrames(Draft draft, Map> draftFrames, String strData, + ByteBuffer byteData) { + if (!draftFrames.containsKey(draft)) { + List frames = null; + if (strData != null) { + frames = draft.createFrames(strData, false); + } + if (byteData != null) { + frames = draft.createFrames(byteData, false); + } + if (frames != null) { + draftFrames.put(draft, frames); + } + } + } + + /** + * This class is used to process incoming data + */ + public class WebSocketWorker extends Thread { + + private BlockingQueue iqueue; + + public WebSocketWorker() { + iqueue = new LinkedBlockingQueue<>(); + setName("WebSocketWorker-" + getId()); + setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + log.error("Uncaught exception in thread {}: {}", t.getName(), e); + } + }); + } + + public void put(WebSocketImpl ws) throws InterruptedException { + iqueue.put(ws); + } + + @Override + public void run() { + WebSocketImpl ws = null; + try { + while (true) { + ByteBuffer buf; + ws = iqueue.take(); + buf = ws.inQueue.poll(); + assert (buf != null); + doDecode(ws, buf); + ws = null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (VirtualMachineError | ThreadDeath | LinkageError e) { + log.error("Got fatal error in worker thread {}", getName()); + Exception exception = new Exception(e); + handleFatal(ws, exception); + } catch (Throwable e) { + log.error("Uncaught exception in thread {}: {}", getName(), e); + if (ws != null) { + Exception exception = new Exception(e); + onWebsocketError(ws, exception); + ws.close(); + } + } + } + + /** + * call ws.decode on the byteBuffer + * + * @param ws the Websocket + * @param buf the buffer to decode to + * @throws InterruptedException thrown by pushBuffer + */ + private void doDecode(WebSocketImpl ws, ByteBuffer buf) throws InterruptedException { + try { + ws.decode(buf); + } catch (Exception e) { + log.error("Error while reading from remote connection", e); + } finally { + pushBuffer(buf); + } + } + } +} diff --git a/src/main/java/org/java_websocket/server/package-info.java b/src/main/java/org/java_websocket/server/package-info.java new file mode 100644 index 0000000..0676baf --- /dev/null +++ b/src/main/java/org/java_websocket/server/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates all implementations in relation with the WebSocketServer. + */ +package org.java_websocket.server; \ No newline at end of file diff --git a/src/main/java/org/java_websocket/util/Base64.java b/src/main/java/org/java_websocket/util/Base64.java new file mode 100644 index 0000000..c2b8031 --- /dev/null +++ b/src/main/java/org/java_websocket/util/Base64.java @@ -0,0 +1,1049 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.util; + +/** + *

Encodes and decodes to and from Base64 notation.

+ *

Homepage: http://iharder.net/base64.

+ * + *

Example:

+ * + * String encoded = Base64.encode( myByteArray ); + *
+ * byte[] myByteArray = Base64.decode( encoded ); + * + *

The options parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as encodeBytes( + * bytes, options ) the options parameter can be used to indicate such things as first gzipping the + * bytes before encoding them, not inserting linefeeds, and encoding using the URL-safe and Ordered + * dialects.

+ * + *

Note, according to RFC3548, + * Section 2.1, implementations should not add line feeds unless explicitly told to do so. I've got + * Base64 set to this behavior now, although earlier versions broke lines by default.

+ * + *

The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:

+ * + * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES + * ); + *

to compress the data before encoding it and then making the output have newline + * characters.

+ *

Also...

+ * String encoded = Base64.encodeBytes( crazyString.getBytes() ); + * + * + * + *

+ * Change Log: + *

+ *
    + *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the + * value 01111111, which is an invalid base 64 character but should not + * throw an ArrayIndexOutOfBoundsException either. Led to discovery of + * mishandling (or potential for better handling) of other bad input + * characters. You should now get an IOException if you try decoding + * something that has bad characters in it.
  • + *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded + * string ended in the last column; the buffer was not properly shrunk and + * contained an extra (null) byte that made it into the string.
  • + *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing + * the Base64.OutputStream closed the Base64 encoding (by padding with equals + * signs) too soon. Also added an option to suppress the automatic decoding + * of gzipped streams. Also added experimental support for specifying a + * class loader when using the method.
  • + *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java + * footprint with its CharEncoders and so forth. Fixed some javadocs that were + * inconsistent. Removed imports and specified things like java.io.IOException + * explicitly inline.
  • + *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the + * final encoded data will be so that the code doesn't have to create two output + * arrays: an oversized initial one and then a final, exact-sized one. Big win + * when using the family of methods (and not + * using the gzip options which uses a different mechanism with streams and stuff).
  • + *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some + * similar helper methods to be more efficient with memory by not returning a + * String but just a byte array.
  • + *
  • v2.3 - This is not a drop-in replacement! This is two years of comments + * and bug fixes queued up and finally executed. Thanks to everyone who sent + * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. + * Much bad coding was cleaned up including throwing exceptions where necessary + * instead of returning null values or something similar. Here are some changes + * that may affect you: + *
      + *
    • Does not break lines, by default. This is to keep in compliance with + * RFC3548.
    • + *
    • Throws exceptions instead of returning null values. Because some operations + * (especially those that may permit the GZIP option) use IO streams, there + * is a possibility of an java.io.IOException being thrown. After some discussion and + * thought, I've changed the behavior of the methods to throw java.io.IOExceptions + * rather than return null if ever there's an error. I think this is more + * appropriate, though it will require some changes to your code. Sorry, + * it should have been done this way to begin with.
    • + *
    • Removed all references to System.out, System.err, and the like. + * Shame on me. All I can say is sorry they were ever there.
    • + *
    • Throws IllegalArgumentExceptions as needed + * such as when passed arrays are null or offsets are invalid.
    • + *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. + * This was especially annoying before for people who were thorough in their + * own projects and then had gobs of javadoc warnings on this file.
    • + *
    + *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug + * when using very small files (~< 40 bytes).
  • + *
  • v2.2 - Added some helper methods for encoding/decoding directly from + * one file to the next. Also added a main() method to support command line + * encoding/decoding from one file to the next. Also added these Base64 dialects: + *
      + *
    1. The default is RFC3548 format.
    2. + *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates + * URL and file name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html
    4. + *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates + * URL and file name friendly format that preserves lexical ordering as described + * in http://www.faqs.org/qa/rfcc-1940.html
    6. + *
    + * Special thanks to Jim Kellerman at http://www.powerset.com/ + * for contributing the new Base64 dialects. + *
  • + * + *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (ints that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using decode( String s, boolean gzipCompressed ). + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).
  • + *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ * + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.7 + */ +public class Base64 { + + /* ******** P U B L I C F I E L D S ******** */ + + + /** + * No options specified. Value is zero. + */ + public static final int NO_OPTIONS = 0; + + /** + * Specify encoding in first bit. Value is one. + */ + public static final int ENCODE = 1; + + /** + * Specify that data should be gzip-compressed in second bit. Value is two. + */ + public static final int GZIP = 2; + + /** + * Do break lines when encoding. Value is 8. + */ + public static final int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described in Section 4 of + * RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * It is important to note that data encoded this way is not officially valid Base64, or + * at the very least should not be called Base64 without also specifying that is was encoded using + * the URL- and Filename-safe dialect. + */ + public static final int URL_SAFE = 16; + + + /** + * Encode using the special "ordered" dialect of Base64 described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + public static final int ORDERED = 32; + + + /* ******** P R I V A T E F I E L D S ******** */ + + + /** + * Maximum line length (76) of Base64 output. + */ + private static final int MAX_LINE_LENGTH = 76; + + + /** + * The equals sign (=) as a byte. + */ + private static final byte EQUALS_SIGN = (byte) '='; + + + /** + * The new line character (\n) as a byte. + */ + private static final byte NEW_LINE = (byte) '\n'; + + + /** + * Preferred encoding. + */ + private static final String PREFERRED_ENCODING = "US-ASCII"; + + + private static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + + + /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** + * The 64 valid Base64 values. + */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private static final byte[] _STANDARD_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' + }; + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a negative number + * indicating some other meaning. + **/ + private static final byte[] _STANDARD_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9, // Decimal 123 - 127 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + + /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private static final byte[] _URL_SAFE_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' + }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private static final byte[] _URL_SAFE_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9, // Decimal 123 - 127 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + + + /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, and it is described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + private static final byte[] _ORDERED_ALPHABET = { + (byte) '-', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) '_', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' + }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private static final byte[] _ORDERED_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' + -9, -9, -9, -9, -9, // Decimal 123 - 127 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + + /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options specified. It's + * possible, though silly, to specify ORDERED and URLSAFE in which case one of them will be + * picked, though there is no guarantee as to which one will be picked. + */ + private static final byte[] getAlphabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } // end getAlphabet + + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on the options specified. It's + * possible, though silly, to specify ORDERED and URL_SAFE in which case one of them will be + * picked, though there is no guarantee as to which one will be picked. + */ + private static final byte[] getDecodabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } // end getAlphabet + + + /** + * Defeats instantiation. + */ + private Base64() { + } + + + + + /* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes up to the first three bytes of array threeBytes and returns a four-byte + * array in Base64 notation. The actual number of significant bytes in your array is given by + * numSigBytes. The array threeBytes needs only be as big as + * numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } // end encode3to4 + + + /** + *

Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. The source and + * destination arrays can be manipulated anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays are large enough to accommodate + * srcOffset + 3 for the source array or destOffset + 4 for the + * destination array. The actual number of significant bytes in your array is given by + * numSigBytes.

+ *

This is the lowest level of the encoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset, int options) { + + final byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws IllegalArgumentException if source array is null + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+   *   GZIP: gzip-compresses object before encoding it.
+   *   DO_BREAK_LINES: break lines at 76 characters
+   *     Note: Technically, this makes your encoding non-compliant.
+   * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, + * it just returned a null value, but in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @throws java.io.IOException if there is an error + * @throws IllegalArgumentException if source array is null, if source array, offset, or length + * are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, int options) + throws java.io.IOException { + byte[] encoded = encodeBytesToBytes(source, off, len, options); + + // Return value according to relevant encoding. + try { + return new String(encoded, PREFERRED_ENCODING); + } catch (java.io.UnsupportedEncodingException uue) { + return new String(encoded); + } // end catch + + } // end encodeBytes + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns a byte array instead of + * instantiating a String. This is more efficient if you're working with I/O streams and have + * large data sets to encode. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @throws java.io.IOException if there is an error + * @throws IllegalArgumentException if source array is null, if source array, offset, or length + * are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) + throws java.io.IOException { + + if (source == null) { + throw new IllegalArgumentException("Cannot serialize a null array."); + } // end if: null + + if (off < 0) { + throw new IllegalArgumentException("Cannot have negative offset: " + off); + } // end if: off < 0 + + if (len < 0) { + throw new IllegalArgumentException("Cannot have length offset: " + len); + } // end if: len < 0 + + if (off + len > source.length) { + throw new IllegalArgumentException( + String + .format("Cannot have offset of %d and length of %d with array of length %d", off, len, + source.length)); + } // end if: off < 0 + + // Compress? + if ((options & GZIP) != 0) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new OutputStream(baos, ENCODE | options); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } finally { + try { + if (gzos != null) { + gzos.close(); + } + } catch (Exception e) { + // do nothing + } + try { + if (b64os != null) { + b64os.close(); + } + } catch (Exception e) { + // do nothing + } + try { + if (baos != null) { + baos.close(); + } + } catch (Exception e) { + // do nothing + } + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) != 0; + + //int len43 = len * 4 / 3; + //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding + if (breakLines) { + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[encLen]; + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + // Only resize array if we didn't guess it right. + if (e <= outBuff.length - 1) { + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff, 0, finalOut, 0, e); + //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); + return finalOut; + } else { + //System.err.println("No need to resize array."); + return outBuff; + } + + } // end else: don't compress + + } // end encodeBytesToBytes + + + + + + /* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source and writes the resulting bytes (up to three of + * them) to destination. The source and destination arrays can be manipulated anywhere + * along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays are large enough to accommodate + * srcOffset + 4 for the source array or destOffset + 3 for the + * destination array. This method returns the actual number of bytes that were + * converted from the Base64 encoding. + *

This is the lowest level of the decoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws IllegalArgumentException if source or destination arrays are null, if srcOffset or + * destOffset are invalid or there is not enough room in the + * array. + * @since 1.3 + */ + private static int decode4to3( + byte[] source, int srcOffset, + byte[] destination, int destOffset, int options) { + + // Lots of error checking and exception throwing + if (source == null) { + throw new IllegalArgumentException("Source array was null."); + } // end if + if (destination == null) { + throw new IllegalArgumentException("Destination array was null."); + } // end if + if (srcOffset < 0 || srcOffset + 3 >= source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", + source.length, srcOffset)); + } // end if + if (destOffset < 0 || destOffset + 2 >= destination.length) { + throw new IllegalArgumentException(String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", + destination.length, destOffset)); + } // end if + + final byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } + } // end decodeToBytes + + + /** + * A {@link OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + + /** + * Constructs a {@link OutputStream} in either ENCODE or DECODE mode. + *

+ * Valid options:

+     *   ENCODE or DECODE: Encode or Decode as data is read.
+     *   DO_BREAK_LINES: don't break lines at 76 characters
+     *     (only meaningful when encoding)
+     * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } // end constructor + + + /** + * Writes the byte to the output stream after converting to/from Base64 notation. When encoding, + * bytes are buffered three at a time before the output stream actually gets a write() call. + * When decoding, bytes are buffered four at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(int theByte) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theByte); + return; + } // end if: suspended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to encode. + + this.out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + this.out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to output. + + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException("Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + /** + * Calls {@link #write(int)} repeatedly until len bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write(byte[] theBytes, int off, int len) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theBytes, off, len); + return; + } // end if: suspended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer without closing the stream. + * + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException("Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + } // end inner class OutputStream +} // end class Base64 diff --git a/src/main/java/org/java_websocket/util/ByteBufferUtils.java b/src/main/java/org/java_websocket/util/ByteBufferUtils.java new file mode 100644 index 0000000..e434479 --- /dev/null +++ b/src/main/java/org/java_websocket/util/ByteBufferUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.util; + +import java.nio.ByteBuffer; + +/** + * Utility class for ByteBuffers + */ +public class ByteBufferUtils { + + /** + * Private constructor for static class + */ + private ByteBufferUtils() { + } + + /** + * Transfer from one ByteBuffer to another ByteBuffer + * + * @param source the ByteBuffer to copy from + * @param dest the ByteBuffer to copy to + * @return the number of transferred bytes + */ + public static int transferByteBuffer(ByteBuffer source, ByteBuffer dest) { + if (source == null || dest == null) { + throw new IllegalArgumentException(); + } + int fremain = source.remaining(); + int toremain = dest.remaining(); + if (fremain > toremain) { + int limit = Math.min(fremain, toremain); + source.limit(limit); + dest.put(source); + return limit; + } else { + dest.put(source); + return fremain; + } + } + + /** + * Get a ByteBuffer with zero capacity + * + * @return empty ByteBuffer + */ + public static ByteBuffer getEmptyByteBuffer() { + return ByteBuffer.allocate(0); + } +} diff --git a/src/main/java/org/java_websocket/util/Charsetfunctions.java b/src/main/java/org/java_websocket/util/Charsetfunctions.java new file mode 100644 index 0000000..0cea88e --- /dev/null +++ b/src/main/java/org/java_websocket/util/Charsetfunctions.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.util; + +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.framing.CloseFrame; + +public class Charsetfunctions { + + /** + * Private constructor for real static class + */ + private Charsetfunctions() { + } + + private static final CodingErrorAction codingErrorAction = CodingErrorAction.REPORT; + + /* + * @return UTF-8 encoding in bytes + */ + public static byte[] utf8Bytes(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + /* + * @return ASCII encoding in bytes + */ + public static byte[] asciiBytes(String s) { + return s.getBytes(StandardCharsets.US_ASCII); + } + + public static String stringAscii(byte[] bytes) { + return stringAscii(bytes, 0, bytes.length); + } + + public static String stringAscii(byte[] bytes, int offset, int length) { + return new String(bytes, offset, length, StandardCharsets.US_ASCII); + } + + public static String stringUtf8(byte[] bytes) throws InvalidDataException { + return stringUtf8(ByteBuffer.wrap(bytes)); + } + + public static String stringUtf8(ByteBuffer bytes) throws InvalidDataException { + CharsetDecoder decode = StandardCharsets.UTF_8.newDecoder(); + decode.onMalformedInput(codingErrorAction); + decode.onUnmappableCharacter(codingErrorAction); + String s; + try { + bytes.mark(); + s = decode.decode(bytes).toString(); + bytes.reset(); + } catch (CharacterCodingException e) { + throw new InvalidDataException(CloseFrame.NO_UTF8, e); + } + return s; + } + + /** + * Implementation of the "Flexible and Economical UTF-8 Decoder" algorithm by Björn Höhrmann + * (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/) + */ + private static final int[] utf8d = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // 00..1f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // 20..3f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // 40..5f + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, // 60..7f + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, // 80..9f + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, // a0..bf + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, // c0..df + 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef + 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, + 1, // s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, + 1, // s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, + 1, // s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + // s7..s8 + }; + + /** + * Check if the provided BytebBuffer contains a valid utf8 encoded string. + *

+ * Using the algorithm "Flexible and Economical UTF-8 Decoder" by Björn Höhrmann + * (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/) + * + * @param data the ByteBuffer + * @param off offset (for performance reasons) + * @return does the ByteBuffer contain a valid utf8 encoded string + */ + public static boolean isValidUTF8(ByteBuffer data, int off) { + int len = data.remaining(); + if (len < off) { + return false; + } + int state = 0; + for (int i = off; i < len; ++i) { + state = utf8d[256 + (state << 4) + utf8d[(0xff & data.get(i))]]; + if (state == 1) { + return false; + } + } + return true; + } + + /** + * Calling isValidUTF8 with offset 0 + * + * @param data the ByteBuffer + * @return does the ByteBuffer contain a valid utf8 encoded string + */ + public static boolean isValidUTF8(ByteBuffer data) { + return isValidUTF8(data, 0); + } + +} diff --git a/src/main/java/org/java_websocket/util/NamedThreadFactory.java b/src/main/java/org/java_websocket/util/NamedThreadFactory.java new file mode 100644 index 0000000..19091c0 --- /dev/null +++ b/src/main/java/org/java_websocket/util/NamedThreadFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.java_websocket.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class NamedThreadFactory implements ThreadFactory { + + private final ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String threadPrefix; + private final boolean daemon; + + public NamedThreadFactory(String threadPrefix) { + this.threadPrefix = threadPrefix; + this.daemon = false; + } + + public NamedThreadFactory(String threadPrefix, boolean daemon) { + this.threadPrefix = threadPrefix; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = defaultThreadFactory.newThread(runnable); + thread.setDaemon(daemon); + thread.setName(threadPrefix + "-" + threadNumber); + return thread; + } +} diff --git a/src/main/java/org/java_websocket/util/package-info.java b/src/main/java/org/java_websocket/util/package-info.java new file mode 100644 index 0000000..af4d1af --- /dev/null +++ b/src/main/java/org/java_websocket/util/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * This package encapsulates the utility classes. + */ +package org.java_websocket.util; \ No newline at end of file diff --git a/src/main/java/org/json/CDL.java b/src/main/java/org/json/CDL.java new file mode 100644 index 0000000..f12cfc0 --- /dev/null +++ b/src/main/java/org/json/CDL.java @@ -0,0 +1,287 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * This provides static methods to convert comma delimited text into a + * JSONArray, and to convert a JSONArray into comma delimited text. Comma + * delimited text is a very popular format for data interchange. It is + * understood by most database, spreadsheet, and organizer programs. + *

+ * Each row of text represents a row in a table or a data record. Each row + * ends with a NEWLINE character. Each row contains one or more values. + * Values are separated by commas. A value can contain any character except + * for comma, unless is is wrapped in single quotes or double quotes. + *

+ * The first row usually contains the names of the columns. + *

+ * A comma delimited list can be converted into a JSONArray of JSONObjects. + * The names for the elements in the JSONObjects can be taken from the names + * in the first row. + * @author JSON.org + * @version 2016-05-01 + */ +public class CDL { + + /** + * Get the next value. The value can be wrapped in quotes. The value can + * be empty. + * @param x A JSONTokener of the source text. + * @return The value string, or null if empty. + * @throws JSONException if the quoted string is badly formed. + */ + private static String getValue(JSONTokener x) throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = x.next(); + } while (c == ' ' || c == '\t'); + switch (c) { + case 0: + return null; + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = x.next(); + if (c == q) { + //Handle escaped double-quote + char nextC = x.next(); + if(nextC != '\"') { + // if our quote was the end of the file, don't step + if(nextC > 0) { + x.back(); + } + break; + } + } + if (c == 0 || c == '\n' || c == '\r') { + throw x.syntaxError("Missing close quote '" + q + "'."); + } + sb.append(c); + } + return sb.toString(); + case ',': + x.back(); + return ""; + default: + x.back(); + return x.nextTo(','); + } + } + + /** + * Produce a JSONArray of strings from a row of comma delimited values. + * @param x A JSONTokener of the source text. + * @return A JSONArray of strings. + * @throws JSONException if a called function fails + */ + public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { + JSONArray ja = new JSONArray(); + for (;;) { + String value = getValue(x); + char c = x.next(); + if (value == null || + (ja.length() == 0 && value.length() == 0 && c != ',')) { + return null; + } + ja.put(value); + for (;;) { + if (c == ',') { + break; + } + if (c != ' ') { + if (c == '\n' || c == '\r' || c == 0) { + return ja; + } + throw x.syntaxError("Bad character '" + c + "' (" + + (int)c + ")."); + } + c = x.next(); + } + } + } + + /** + * Produce a JSONObject from a row of comma delimited text, using a + * parallel JSONArray of strings to provides the names of the elements. + * @param names A JSONArray of names. This is commonly obtained from the + * first row of a comma delimited text file using the rowToJSONArray + * method. + * @param x A JSONTokener of the source text. + * @return A JSONObject combining the names and values. + * @throws JSONException if a called function fails + */ + public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) + throws JSONException { + JSONArray ja = rowToJSONArray(x); + return ja != null ? ja.toJSONObject(names) : null; + } + + /** + * Produce a comma delimited text row from a JSONArray. Values containing + * the comma character will be quoted. Troublesome characters may be + * removed. + * @param ja A JSONArray of strings. + * @return A string ending in NEWLINE. + */ + public static String rowToString(JSONArray ja) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + if (i > 0) { + sb.append(','); + } + Object object = ja.opt(i); + if (object != null) { + String string = object.toString(); + if (string.length() > 0 && (string.indexOf(',') >= 0 || + string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || + string.indexOf(0) >= 0 || string.charAt(0) == '"')) { + sb.append('"'); + int length = string.length(); + for (int j = 0; j < length; j += 1) { + char c = string.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } else { + sb.append(string); + } + } + } + sb.append('\n'); + return sb.toString(); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException if a called function fails + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param x The JSONTokener containing the comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException if a called function fails + */ + public static JSONArray toJSONArray(JSONTokener x) throws JSONException { + return toJSONArray(rowToJSONArray(x), x); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException if a called function fails + */ + public static JSONArray toJSONArray(JSONArray names, String string) + throws JSONException { + return toJSONArray(names, new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param x A JSONTokener of the source text. + * @return A JSONArray of JSONObjects. + * @throws JSONException if a called function fails + */ + public static JSONArray toJSONArray(JSONArray names, JSONTokener x) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (;;) { + JSONObject jo = rowToJSONObject(names, x); + if (jo == null) { + break; + } + ja.put(jo); + } + if (ja.length() == 0) { + return null; + } + return ja; + } + + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects. The + * first row will be a list of names obtained by inspecting the first + * JSONObject. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException if a called function fails + */ + public static String toString(JSONArray ja) throws JSONException { + JSONObject jo = ja.optJSONObject(0); + if (jo != null) { + JSONArray names = jo.names(); + if (names != null) { + return rowToString(names) + toString(names, ja); + } + } + return null; + } + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects using + * a provided list of names. The list of names is not included in the + * output. + * @param names A JSONArray of strings. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException if a called function fails + */ + public static String toString(JSONArray names, JSONArray ja) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + JSONObject jo = ja.optJSONObject(i); + if (jo != null) { + sb.append(rowToString(jo.toJSONArray(names))); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/json/Cookie.java b/src/main/java/org/json/Cookie.java new file mode 100644 index 0000000..a43d1ed --- /dev/null +++ b/src/main/java/org/json/Cookie.java @@ -0,0 +1,224 @@ +package org.json; + +import java.util.Locale; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Convert a web browser cookie specification to a JSONObject and back. + * JSON and Cookies are both notations for name/value pairs. + * See also: https://tools.ietf.org/html/rfc6265 + * @author JSON.org + * @version 2015-12-09 + */ +public class Cookie { + + /** + * Produce a copy of a string in which the characters '+', '%', '=', ';' + * and control characters are replaced with "%hh". This is a gentle form + * of URL encoding, attempting to cause as little distortion to the + * string as possible. The characters '=' and ';' are meta characters in + * cookies. By convention, they are escaped using the URL-encoding. This is + * only a convention, not a standard. Often, cookies are expected to have + * encoded values. We encode '=' and ';' because we must. We encode '%' and + * '+' because they are meta characters in URL encoding. + * @param string The source string. + * @return The escaped result. + */ + public static String escape(String string) { + char c; + String s = string.trim(); + int length = s.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i += 1) { + c = s.charAt(i); + if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { + sb.append('%'); + sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); + sb.append(Character.forDigit((char)(c & 0x0f), 16)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + + /** + * Convert a cookie specification string into a JSONObject. The string + * must contain a name value pair separated by '='. The name and the value + * will be unescaped, possibly converting '+' and '%' sequences. The + * cookie properties may follow, separated by ';', also represented as + * name=value (except the Attribute properties like "Secure" or "HttpOnly", + * which do not have a value. The value {@link Boolean#TRUE} will be used for these). + * The name will be stored under the key "name", and the value will be + * stored under the key "value". This method does not do checking or + * validation of the parameters. It only converts the cookie string into + * a JSONObject. All attribute names are converted to lower case keys in the + * JSONObject (HttpOnly => httponly). If an attribute is specified more than + * once, only the value found closer to the end of the cookie-string is kept. + * @param string The cookie specification string. + * @return A JSONObject containing "name", "value", and possibly other + * members. + * @throws JSONException If there is an error parsing the Cookie String. + * Cookie strings must have at least one '=' character and the 'name' + * portion of the cookie must not be blank. + */ + public static JSONObject toJSONObject(String string) { + final JSONObject jo = new JSONObject(); + String name; + Object value; + + + JSONTokener x = new JSONTokener(string); + + name = unescape(x.nextTo('=').trim()); + //per RFC6265, if the name is blank, the cookie should be ignored. + if("".equals(name)) { + throw new JSONException("Cookies must have a 'name'"); + } + jo.put("name", name); + // per RFC6265, if there is no '=', the cookie should be ignored. + // the 'next' call here throws an exception if the '=' is not found. + x.next('='); + jo.put("value", unescape(x.nextTo(';')).trim()); + // discard the ';' + x.next(); + // parse the remaining cookie attributes + while (x.more()) { + name = unescape(x.nextTo("=;")).trim().toLowerCase(Locale.ROOT); + // don't allow a cookies attributes to overwrite it's name or value. + if("name".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'name'"); + } + if("value".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'value'"); + } + // check to see if it's a flag property + if (x.next() != '=') { + value = Boolean.TRUE; + } else { + value = unescape(x.nextTo(';')).trim(); + x.next(); + } + // only store non-blank attributes + if(!"".equals(name) && !"".equals(value)) { + jo.put(name, value); + } + } + return jo; + } + + + /** + * Convert a JSONObject into a cookie specification string. The JSONObject + * must contain "name" and "value" members (case insensitive). + * If the JSONObject contains other members, they will be appended to the cookie + * specification string. User-Agents are instructed to ignore unknown attributes, + * so ensure your JSONObject is using only known attributes. + * See also: https://tools.ietf.org/html/rfc6265 + * @param jo A JSONObject + * @return A cookie specification string + * @throws JSONException thrown if the cookie has no name. + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + + String name = null; + Object value = null; + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key)) { + name = jo.getString(key).trim(); + } + if("value".equalsIgnoreCase(key)) { + value=jo.getString(key).trim(); + } + if(name != null && value != null) { + break; + } + } + + if(name == null || "".equals(name.trim())) { + throw new JSONException("Cookie does not have a name"); + } + if(value == null) { + value = ""; + } + + sb.append(escape(name)); + sb.append("="); + sb.append(escape((String)value)); + + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key) + || "value".equalsIgnoreCase(key)) { + // already processed above + continue; + } + value = jo.opt(key); + if(value instanceof Boolean) { + if(Boolean.TRUE.equals(value)) { + sb.append(';').append(escape(key)); + } + // don't emit false values + } else { + sb.append(';') + .append(escape(key)) + .append('=') + .append(escape(value.toString())); + } + } + + return sb.toString(); + } + + /** + * Convert %hh sequences to single characters, and + * convert plus to space. + * @param string A string that may contain + * + (plus) and + * %hh sequences. + * @return The unescaped string. + */ + public static String unescape(String string) { + int length = string.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + char c = string.charAt(i); + if (c == '+') { + c = ' '; + } else if (c == '%' && i + 2 < length) { + int d = JSONTokener.dehexchar(string.charAt(i + 1)); + int e = JSONTokener.dehexchar(string.charAt(i + 2)); + if (d >= 0 && e >= 0) { + c = (char)(d * 16 + e); + i += 2; + } + } + sb.append(c); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/json/CookieList.java b/src/main/java/org/json/CookieList.java new file mode 100644 index 0000000..83b2630 --- /dev/null +++ b/src/main/java/org/json/CookieList.java @@ -0,0 +1,86 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * Convert a web browser cookie list string to a JSONObject and back. + * @author JSON.org + * @version 2015-12-09 + */ +public class CookieList { + + /** + * Convert a cookie list into a JSONObject. A cookie list is a sequence + * of name/value pairs. The names are separated from the values by '='. + * The pairs are separated by ';'. The names and the values + * will be unescaped, possibly converting '+' and '%' sequences. + * + * To add a cookie to a cookie list, + * cookielistJSONObject.put(cookieJSONObject.getString("name"), + * cookieJSONObject.getString("value")); + * @param string A cookie list string + * @return A JSONObject + * @throws JSONException if a called function fails + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + JSONTokener x = new JSONTokener(string); + while (x.more()) { + String name = Cookie.unescape(x.nextTo('=')); + x.next('='); + jo.put(name, Cookie.unescape(x.nextTo(';'))); + x.next(); + } + return jo; + } + + /** + * Convert a JSONObject into a cookie list. A cookie list is a sequence + * of name/value pairs. The names are separated from the values by '='. + * The pairs are separated by ';'. The characters '%', '+', '=', and ';' + * in the names and values are replaced by "%hh". + * @param jo A JSONObject + * @return A cookie list string + * @throws JSONException if a called function fails + */ + public static String toString(JSONObject jo) throws JSONException { + boolean b = false; + final StringBuilder sb = new StringBuilder(); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + final Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + if (b) { + sb.append(';'); + } + sb.append(Cookie.escape(key)); + sb.append("="); + sb.append(Cookie.escape(value.toString())); + b = true; + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/json/HTTP.java b/src/main/java/org/json/HTTP.java new file mode 100644 index 0000000..cc01167 --- /dev/null +++ b/src/main/java/org/json/HTTP.java @@ -0,0 +1,162 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Locale; + +/** + * Convert an HTTP header to a JSONObject and back. + * @author JSON.org + * @version 2015-12-09 + */ +public class HTTP { + + /** Carriage return/line feed. */ + public static final String CRLF = "\r\n"; + + /** + * Convert an HTTP header string into a JSONObject. It can be a request + * header or a response header. A request header will contain + *

{
+     *    Method: "POST" (for example),
+     *    "Request-URI": "/" (for example),
+     *    "HTTP-Version": "HTTP/1.1" (for example)
+     * }
+ * A response header will contain + *
{
+     *    "HTTP-Version": "HTTP/1.1" (for example),
+     *    "Status-Code": "200" (for example),
+     *    "Reason-Phrase": "OK" (for example)
+     * }
+ * In addition, the other parameters in the header will be captured, using + * the HTTP field names as JSON names, so that
{@code
+     *    Date: Sun, 26 May 2002 18:06:04 GMT
+     *    Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s
+     *    Cache-Control: no-cache}
+ * become + *
{@code
+     *    Date: "Sun, 26 May 2002 18:06:04 GMT",
+     *    Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s",
+     *    "Cache-Control": "no-cache",
+     * ...}
+ * It does no further checking or conversion. It does not parse dates. + * It does not do '%' transforms on URLs. + * @param string An HTTP header string. + * @return A JSONObject containing the elements and attributes + * of the XML string. + * @throws JSONException if a called function fails + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + HTTPTokener x = new HTTPTokener(string); + String token; + + token = x.nextToken(); + if (token.toUpperCase(Locale.ROOT).startsWith("HTTP")) { + +// Response + + jo.put("HTTP-Version", token); + jo.put("Status-Code", x.nextToken()); + jo.put("Reason-Phrase", x.nextTo('\0')); + x.next(); + + } else { + +// Request + + jo.put("Method", token); + jo.put("Request-URI", x.nextToken()); + jo.put("HTTP-Version", x.nextToken()); + } + +// Fields + + while (x.more()) { + String name = x.nextTo(':'); + x.next(':'); + jo.put(name, x.nextTo('\0')); + x.next(); + } + return jo; + } + + + /** + * Convert a JSONObject into an HTTP header. A request header must contain + *
{
+     *    Method: "POST" (for example),
+     *    "Request-URI": "/" (for example),
+     *    "HTTP-Version": "HTTP/1.1" (for example)
+     * }
+ * A response header must contain + *
{
+     *    "HTTP-Version": "HTTP/1.1" (for example),
+     *    "Status-Code": "200" (for example),
+     *    "Reason-Phrase": "OK" (for example)
+     * }
+ * Any other members of the JSONObject will be output as HTTP fields. + * The result will end with two CRLF pairs. + * @param jo A JSONObject + * @return An HTTP header string. + * @throws JSONException if the object does not contain enough + * information. + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { + sb.append(jo.getString("HTTP-Version")); + sb.append(' '); + sb.append(jo.getString("Status-Code")); + sb.append(' '); + sb.append(jo.getString("Reason-Phrase")); + } else if (jo.has("Method") && jo.has("Request-URI")) { + sb.append(jo.getString("Method")); + sb.append(' '); + sb.append('"'); + sb.append(jo.getString("Request-URI")); + sb.append('"'); + sb.append(' '); + sb.append(jo.getString("HTTP-Version")); + } else { + throw new JSONException("Not enough material for an HTTP header."); + } + sb.append(CRLF); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + String value = jo.optString(key); + if (!"HTTP-Version".equals(key) && !"Status-Code".equals(key) && + !"Reason-Phrase".equals(key) && !"Method".equals(key) && + !"Request-URI".equals(key) && !JSONObject.NULL.equals(value)) { + sb.append(key); + sb.append(": "); + sb.append(jo.optString(key)); + sb.append(CRLF); + } + } + sb.append(CRLF); + return sb.toString(); + } +} diff --git a/src/main/java/org/json/HTTPTokener.java b/src/main/java/org/json/HTTPTokener.java new file mode 100644 index 0000000..16c7081 --- /dev/null +++ b/src/main/java/org/json/HTTPTokener.java @@ -0,0 +1,77 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The HTTPTokener extends the JSONTokener to provide additional methods + * for the parsing of HTTP headers. + * @author JSON.org + * @version 2015-12-09 + */ +public class HTTPTokener extends JSONTokener { + + /** + * Construct an HTTPTokener from a string. + * @param string A source string. + */ + public HTTPTokener(String string) { + super(string); + } + + + /** + * Get the next token or string. This is used in parsing HTTP headers. + * @return A String. + * @throws JSONException if a syntax error occurs + */ + public String nextToken() throws JSONException { + char c; + char q; + StringBuilder sb = new StringBuilder(); + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == '"' || c == '\'') { + q = c; + for (;;) { + c = next(); + if (c < ' ') { + throw syntaxError("Unterminated string."); + } + if (c == q) { + return sb.toString(); + } + sb.append(c); + } + } + for (;;) { + if (c == 0 || Character.isWhitespace(c)) { + return sb.toString(); + } + sb.append(c); + c = next(); + } + } +} diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java new file mode 100644 index 0000000..2a8a1d2 --- /dev/null +++ b/src/main/java/org/json/JSONArray.java @@ -0,0 +1,1714 @@ +package org.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there is , + *  (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
  • + *
+ * + * @author JSON.org + * @version 2016-08/15 + */ +public class JSONArray implements Iterable { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x + * A JSONTokener + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source + * A string that begins with [ (left + * bracket) and ends with ] + *  (right bracket). + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection + * A Collection. + */ + public JSONArray(Collection collection) { + if (collection == null) { + this.myArrayList = new ArrayList(); + } else { + this.myArrayList = new ArrayList(collection.size()); + this.addAll(collection, true); + } + } + + /** + * Construct a JSONArray from an Iterable. This is a shallow copy. + * + * @param iter + * A Iterable collection. + */ + public JSONArray(Iterable iter) { + this(); + if (iter == null) { + return; + } + this.addAll(iter, true); + } + + /** + * Construct a JSONArray from another JSONArray. This is a shallow copy. + * + * @param array + * A array. + */ + public JSONArray(JSONArray array) { + if (array == null) { + this.myArrayList = new ArrayList(); + } else { + // shallow copy directly the internal array lists as any wrapping + // should have been done already in the original JSONArray + this.myArrayList = new ArrayList(array.myArrayList); + } + } + + /** + * Construct a JSONArray from an array. + * + * @param array + * Array. If the parameter passed is null, or not an array, an + * exception will be thrown. + * + * @throws JSONException + * If not an array or if an array value is non-finite number. + * @throws NullPointerException + * Thrown if the array parameter is null. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (!array.getClass().isArray()) { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + this.addAll(array, true); + } + + /** + * Construct a JSONArray with the specified initial capacity. + * + * @param initialCapacity + * the initial capacity of the JSONArray. + * @throws JSONException + * If the initial capacity is negative. + */ + public JSONArray(int initialCapacity) throws JSONException { + if (initialCapacity < 0) { + throw new JSONException( + "JSONArray initial capacity cannot be negative."); + } + this.myArrayList = new ArrayList(initialCapacity); + } + + @Override + public Iterator iterator() { + return this.myArrayList.iterator(); + } + + /** + * Get the object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException + * If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with an index. The string values "true" + * and "false" are converted to boolean. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException + * If there is no value for the index or if the value is not + * convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(index, "boolean", null); + } + + /** + * Get the double value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public double getDouble(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "double", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Float)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "float", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "number", e); + } + } + + /** + * Get the enum value associated with an index. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, int index) throws JSONException { + E val = optEnum(clazz, index); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(index, "enum of type " + + JSONObject.quote(clazz.getSimpleName()), null); + } + return val; + } + + /** + * Get the BigDecimal value associated with an index. If the value is float + * or double, the the {@link BigDecimal#BigDecimal(double)} constructor + * will be used. See notes on the constructor for conversion issues that + * may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigDecimal. + */ + public BigDecimal getBigDecimal (int index) throws JSONException { + Object object = this.get(index); + BigDecimal val = JSONObject.objectToBigDecimal(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigDecimal", object, null); + } + return val; + } + + /** + * Get the BigInteger value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigInteger. + */ + public BigInteger getBigInteger (int index) throws JSONException { + Object object = this.get(index); + BigInteger val = JSONObject.objectToBigInteger(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigInteger", object, null); + } + return val; + } + + /** + * Get the int value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "int", e); + } + } + + /** + * Get the JSONArray associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException + * If there is no value for the index. or if the value is not a + * JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(index, "JSONArray", null); + } + + /** + * Get the JSONObject associated with an index. + * + * @param index + * subscript + * @return A JSONObject value. + * @throws JSONException + * If there is no value for the index or if the value is not a + * JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(index, "JSONObject", null); + } + + /** + * Get the long value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public long getLong(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "long", e); + } + } + + /** + * Get the string associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException + * If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(index, "String", null); + } + + /** + * Determine if the value is null. + * + * @param index + * The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. Warning: + * This method assumes that the data structure is acyclical. + * + * @param separator + * A string that will be inserted between the elements. + * @return a string. + * @throws JSONException + * If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.length(); + if (len == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder( + JSONObject.valueToString(this.myArrayList.get(0))); + + for (int i = 1; i < len; i++) { + sb.append(separator) + .append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + /** + * Removes all of the elements from this JSONArray. + * The JSONArray will be empty after this call returns. + */ + public void clear() { + this.myArrayList.clear(); + } + + /** + * Get the optional object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. If not, null is returned. + * @return An object value, or null if there is no object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList + .get(index); + } + + /** + * Get the optional boolean value associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + /** + * Get the optional boolean value associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional float value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + /** + * Get the optional float value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(int index, float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + + /** + * Get the optional int value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return this.optInt(index, 0); + } + + /** + * Get the optional int value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location or null if not found + */ + public > E optEnum(Class clazz, int index) { + return this.optEnum(clazz, index, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default in case the value is not found + * @return The enum value at the index location or defaultValue if + * the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, int index, E defaultValue) { + try { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigInteger(val, defaultValue); + } + + /** + * Get the optional BigDecimal value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. If the value + * is float or double, the the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigDecimal(val, defaultValue); + } + + /** + * Get the optional JSONArray associated with an index. + * + * @param index + * subscript + * @return A JSONArray value, or null if the index has no value, or if the + * value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get the optional JSONObject associated with an index. Null is returned if + * the key is not found, or null if the index has no value, or if the value + * is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + /** + * Get the optional long value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return this.optLong(index, 0); + } + + /** + * Get the optional long value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object which is the value. + */ + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value is not a + * string and is not null, then it is converted to a string. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return this.optString(index, ""); + } + + /** + * Get the optional string associated with an index. The defaultValue is + * returned if the key is not found. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object + .toString(); + } + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value + * A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + return this.put(value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Collection value) { + return this.put(new JSONArray(value)); + } + + /** + * Append a double value. This increases the array's length by one. + * + * @param value + * A double value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(double value) throws JSONException { + return this.put(Double.valueOf(value)); + } + + /** + * Append a float value. This increases the array's length by one. + * + * @param value + * A float value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(float value) throws JSONException { + return this.put(Float.valueOf(value)); + } + + /** + * Append an int value. This increases the array's length by one. + * + * @param value + * An int value. + * @return this. + */ + public JSONArray put(int value) { + return this.put(Integer.valueOf(value)); + } + + /** + * Append an long value. This increases the array's length by one. + * + * @param value + * A long value. + * @return this. + */ + public JSONArray put(long value) { + return this.put(Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject which + * is produced from a Map. + * + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(Map value) { + return this.put(new JSONObject(value)); + } + + /** + * Append an object value. This increases the array's length by one. + * + * @param value + * An object value. The value should be a Boolean, Double, + * Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Object value) { + JSONObject.testValidity(value); + this.myArrayList.add(value); + return this; + } + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * A boolean value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param index + * The subscript. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + return this.put(index, new JSONArray(value)); + } + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A double value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, double value) throws JSONException { + return this.put(index, Double.valueOf(value)); + } + + /** + * Put or replace a float value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A float value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, float value) throws JSONException { + return this.put(index, Float.valueOf(value)); + } + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * An int value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + return this.put(index, Integer.valueOf(value)); + } + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A long value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + return this.put(index, Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index + * The subscript. + * @param value + * The Map value. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Object value) throws JSONException { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + JSONObject.testValidity(value); + this.myArrayList.set(index, value); + return this; + } + if(index == this.length()){ + // simple append + return this.put(value); + } + // if we are inserting past the length, we want to grow the array all at once + // instead of incrementally. + this.myArrayList.ensureCapacity(index + 1); + while (index != this.length()) { + // we don't need to test validity of NULL objects + this.myArrayList.add(JSONObject.NULL); + } + return this.put(value); + } + + /** + * Put a collection's elements in to the JSONArray. + * + * @param collection + * A Collection. + * @return this. + */ + public JSONArray putAll(Collection collection) { + this.addAll(collection, false); + return this; + } + + /** + * Put an Iterable's elements in to the JSONArray. + * + * @param iter + * An Iterable. + * @return this. + */ + public JSONArray putAll(Iterable iter) { + this.addAll(iter, false); + return this; + } + + /** + * Put a JSONArray's elements in to the JSONArray. + * + * @param array + * A JSONArray. + * @return this. + */ + public JSONArray putAll(JSONArray array) { + // directly copy the elements from the source array to this one + // as all wrapping should have been done already in the source. + this.myArrayList.addAll(array.myArrayList); + return this; + } + + /** + * Put an array's elements in to the JSONArray. + * + * @param array + * Array. If the parameter passed is null, or not an array or Iterable, an + * exception will be thrown. + * @return this. + * + * @throws JSONException + * If not an array, JSONArray, Iterable or if an value is non-finite number. + * @throws NullPointerException + * Thrown if the array parameter is null. + */ + public JSONArray putAll(Object array) throws JSONException { + this.addAll(array, false); + return this; + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer string: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Remove an index and close the hole. + * + * @param index + * The index of the element to be removed. + * @return The value that was associated with the index, or null if there + * was no value. + */ + public Object remove(int index) { + return index >= 0 && index < this.length() + ? this.myArrayList.remove(index) + : null; + } + + /** + * Determine if two JSONArrays are similar. + * They must contain similar sequences. + * + * @param other The other JSONArray + * @return true if they are equal + */ + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray)other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.myArrayList.get(i); + Object valueOther = ((JSONArray)other).myArrayList.get(i); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof Number && valueOther instanceof Number) { + return JSONObject.isNumberSimilar((Number)valueThis, (Number)valueOther); + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + /** + * Produce a JSONObject by combining a JSONArray of names with the values of + * this JSONArray. + * + * @param names + * A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException + * If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.isEmpty() || this.isEmpty()) { + return null; + } + JSONObject jo = new JSONObject(names.length()); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + /** + * Make a JSON text of this JSONArray. For compactness, no unnecessary + * whitespace is added. If it is not possible to produce a syntactically + * correct JSON text then null will be returned instead. This could occur if + * the array contains an invalid number. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, transmittable representation of the + * array. + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONArray. + * + *

If

 {@code indentFactor > 0}
and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *
{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, transmittable representation of the + * object, beginning with [ (left + * bracket) and ending with ] + *  (right bracket). + * @throws JSONException if a called function fails + */ + public String toString(int indentFactor) throws JSONException { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.write(sw, indentFactor, 0).toString(); + } + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param writer the writer object + * @return The writer. + * @throws JSONException if a called function fails + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. + * + *

If

{@code indentFactor > 0}
and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *
{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException if a called function fails or unable to write + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Returns a java.util.List containing all of the elements in this array. + * If an element in the array is a JSONArray or JSONObject it will also + * be converted to a List and a Map respectively. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.List containing the elements of this array + */ + public List toList() { + List results = new ArrayList(this.myArrayList.size()); + for (Object element : this.myArrayList) { + if (element == null || JSONObject.NULL.equals(element)) { + results.add(null); + } else if (element instanceof JSONArray) { + results.add(((JSONArray) element).toList()); + } else if (element instanceof JSONObject) { + results.add(((JSONObject) element).toMap()); + } else { + results.add(element); + } + } + return results; + } + + /** + * Check if JSONArray is empty. + * + * @return true if JSONArray is empty, otherwise false. + */ + public boolean isEmpty() { + return this.myArrayList.isEmpty(); + } + + /** + * Add a collection's elements to the JSONArray. + * + * @param collection + * A Collection. + * @param wrap + * {@code true} to call {@link JSONObject#wrap(Object)} for each item, + * {@code false} to add the items directly + * + */ + private void addAll(Collection collection, boolean wrap) { + this.myArrayList.ensureCapacity(this.myArrayList.size() + collection.size()); + if (wrap) { + for (Object o: collection){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: collection){ + this.put(o); + } + } + } + + /** + * Add an Iterable's elements to the JSONArray. + * + * @param iter + * An Iterable. + * @param wrap + * {@code true} to call {@link JSONObject#wrap(Object)} for each item, + * {@code false} to add the items directly + */ + private void addAll(Iterable iter, boolean wrap) { + if (wrap) { + for (Object o: iter){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: iter){ + this.put(o); + } + } + } + + /** + * Add an array's elements to the JSONArray. + * + * @param array + * Array. If the parameter passed is null, or not an array, + * JSONArray, Collection, or Iterable, an exception will be + * thrown. + * @param wrap + * {@code true} to call {@link JSONObject#wrap(Object)} for each item, + * {@code false} to add the items directly + * + * @throws JSONException + * If not an array or if an array value is non-finite number. + * @throws NullPointerException + * Thrown if the array parameter is null. + */ + private void addAll(Object array, boolean wrap) throws JSONException { + if (array.getClass().isArray()) { + int length = Array.getLength(array); + this.myArrayList.ensureCapacity(this.myArrayList.size() + length); + if (wrap) { + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + for (int i = 0; i < length; i += 1) { + this.put(Array.get(array, i)); + } + } + } else if (array instanceof JSONArray) { + // use the built in array list `addAll` as all object + // wrapping should have been completed in the original + // JSONArray + this.myArrayList.addAll(((JSONArray)array).myArrayList); + } else if (array instanceof Collection) { + this.addAll((Collection)array, wrap); + } else if (array instanceof Iterable) { + this.addAll((Iterable)array, wrap); + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param idx index of the item + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + "." + , cause); + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param idx index of the item + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + " (" + value + ")." + , cause); + } + +} diff --git a/src/main/java/org/json/JSONException.java b/src/main/java/org/json/JSONException.java new file mode 100644 index 0000000..ab7ff77 --- /dev/null +++ b/src/main/java/org/json/JSONException.java @@ -0,0 +1,69 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONException extends RuntimeException { + /** Serialization ID */ + private static final long serialVersionUID = 0; + + /** + * Constructs a JSONException with an explanatory message. + * + * @param message + * Detail about the reason for the exception. + */ + public JSONException(final String message) { + super(message); + } + + /** + * Constructs a JSONException with an explanatory message and cause. + * + * @param message + * Detail about the reason for the exception. + * @param cause + * The cause. + */ + public JSONException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new JSONException with the specified cause. + * + * @param cause + * The cause. + */ + public JSONException(final Throwable cause) { + super(cause.getMessage(), cause); + } + +} diff --git a/src/main/java/org/json/JSONML.java b/src/main/java/org/json/JSONML.java new file mode 100644 index 0000000..aafdf72 --- /dev/null +++ b/src/main/java/org/json/JSONML.java @@ -0,0 +1,542 @@ +package org.json; + +/* +Copyright (c) 2008 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * This provides static methods to convert an XML text into a JSONArray or + * JSONObject, and to covert a JSONArray or JSONObject into an XML text using + * the JsonML transform. + * + * @author JSON.org + * @version 2016-01-30 + */ +public class JSONML { + /** + * Parse XML values and store them in a JSONArray. + * @param x The XMLTokener containing the source string. + * @param arrayForm true if array form, false if object form. + * @param ja The JSONArray that is containing the current tag or null + * if we are at the outermost level. + * @param keepStrings Don't type-convert text nodes and attribute values + * @return A JSONArray if the value is the outermost tag, otherwise null. + * @throws JSONException if a parsing error occurs + */ + private static Object parse( + XMLTokener x, + boolean arrayForm, + JSONArray ja, + boolean keepStrings + ) throws JSONException { + String attribute; + char c; + String closeTag = null; + int i; + JSONArray newja = null; + JSONObject newjo = null; + Object token; + String tagName = null; + +// Test for and skip past these forms: +// +// +// +// + + while (true) { + if (!x.more()) { + throw x.syntaxError("Bad XML"); + } + token = x.nextContent(); + if (token == XML.LT) { + token = x.nextToken(); + if (token instanceof Character) { + if (token == XML.SLASH) { + +// Close tag "); + } else { + x.back(); + } + } else if (c == '[') { + token = x.nextToken(); + if (token.equals("CDATA") && x.next() == '[') { + if (ja != null) { + ja.put(x.nextCDATA()); + } + } else { + throw x.syntaxError("Expected 'CDATA['"); + } + } else { + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + } + } else if (token == XML.QUEST) { + +// "); + } else { + throw x.syntaxError("Misshaped tag"); + } + +// Open tag < + + } else { + if (!(token instanceof String)) { + throw x.syntaxError("Bad tagName '" + token + "'."); + } + tagName = (String)token; + newja = new JSONArray(); + newjo = new JSONObject(); + if (arrayForm) { + newja.put(tagName); + if (ja != null) { + ja.put(newja); + } + } else { + newjo.put("tagName", tagName); + if (ja != null) { + ja.put(newjo); + } + } + token = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (!(token instanceof String)) { + break; + } + +// attribute = value + + attribute = (String)token; + if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { + throw x.syntaxError("Reserved attribute."); + } + token = x.nextToken(); + if (token == XML.EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); + token = null; + } else { + newjo.accumulate(attribute, ""); + } + } + if (arrayForm && newjo.length() > 0) { + newja.put(newjo); + } + +// Empty tag <.../> + + if (token == XML.SLASH) { + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + +// Content, between <...> and + + } else { + if (token != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + closeTag = (String)parse(x, arrayForm, newja, keepStrings); + if (closeTag != null) { + if (!closeTag.equals(tagName)) { + throw x.syntaxError("Mismatched '" + tagName + + "' and '" + closeTag + "'"); + } + tagName = null; + if (!arrayForm && newja.length() > 0) { + newjo.put("childNodes", newja); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + } + } + } + } else { + if (ja != null) { + ja.put(token instanceof String + ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) + : token); + } + } + } + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child tags. + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The source string. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONArray + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child tags. + * As opposed to toJSONArray this method does not attempt to convert + * any text node or attribute value to any type + * but just leaves it as a string. + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The source string. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONArray + */ + public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child content and tags. + * As opposed to toJSONArray this method does not attempt to convert + * any text node or attribute value to any type + * but just leaves it as a string. + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONArray + */ + public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONArray)parse(x, true, null, keepStrings); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child content and tags. + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONArray + */ + public static JSONArray toJSONArray(XMLTokener x) throws JSONException { + return (JSONArray)parse(x, true, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x) throws JSONException { + return (JSONObject)parse(x, false, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONObject)parse(x, false, null, keepStrings); + } + + + /** + * Reverse the JSONML transformation, making an XML text from a JSONArray. + * @param ja A JSONArray. + * @return An XML string. + * @throws JSONException Thrown on error converting to a string + */ + public static String toString(JSONArray ja) throws JSONException { + int i; + JSONObject jo; + int length; + Object object; + StringBuilder sb = new StringBuilder(); + String tagName; + +// Emit = length) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + do { + object = ja.get(i); + i += 1; + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } while (i < length); + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } + + /** + * Reverse the JSONML transformation, making an XML text from a JSONObject. + * The JSONObject must contain a "tagName" property. If it has children, + * then it must have a "childNodes" property containing an array of objects. + * The other properties are attributes with string values. + * @param jo A JSONObject. + * @return An XML string. + * @throws JSONException Thrown on error converting to a string + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + int i; + JSONArray ja; + int length; + Object object; + String tagName; + Object value; + +//Emit '); + } else { + sb.append('>'); + length = ja.length(); + for (i = 0; i < length; i += 1) { + object = ja.get(i); + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java new file mode 100644 index 0000000..8bbb874 --- /dev/null +++ b/src/main/java/org/json/JSONObject.java @@ -0,0 +1,2648 @@ +package org.json; + +import java.io.Closeable; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + * + *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ * + * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2016-08-15 + */ +public class JSONObject { + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object + * An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + /** + * A Null object is equal to the null value and to itself. + * + * @return always returns 0. + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + } + + /** + * Regular Expression Pattern that matches JSON Numbers. This is primarily used for + * output to guarantee that we are always writing valid JSON. + */ + static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + // HashMap is used on purpose to ensure that elements are unordered by + // the specification. + // JSON tends to be a portable transfer format to allows the container + // implementations to rearrange their items for a faster element + // retrieval based on associative access. + // Therefore, an implementation mustn't rely on the order of the item. + this.map = new HashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo + * A JSONObject. + * @param names + * An array of strings. + */ + public JSONObject(JSONObject jo, String ... names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x + * A JSONTokener object containing the source string. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param m + * A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONObject(Map m) { + if (m == null) { + this.map = new HashMap(); + } else { + this.map = new HashMap(m.size()); + for (final Entry e : m.entrySet()) { + if(e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + *

+ * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + *

+ * Methods that are static, return void, + * have parameters, or are "bridge" methods, are ignored. + *

+ * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + *

+ * The {@link JSONPropertyName} annotation can be used on a bean getter to + * override key name used in the JSONObject. For example, using the object + * above with the getName method, if we annotated it with: + *

+     * @JSONPropertyName("FullName")
+     * public String getName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * Similarly, the {@link JSONPropertyName} annotation can be used on non- + * get and is methods. We can also override key + * name used in the JSONObject as seen below even though the field would normally + * be ignored: + *

+     * @JSONPropertyName("FullName")
+     * public String fullName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property + * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and + * {@link JSONPropertyName} are defined on the same method, a depth comparison is + * performed and the one closest to the concrete class being serialized is used. + * If both annotations are at the same level, then the {@link JSONPropertyIgnore} + * annotation takes precedent and the field is not serialized. + * For example, the following declaration would prevent the getName + * method from being serialized: + *

+     * @JSONPropertyName("FullName")
+     * @JSONPropertyIgnore
+     * public String getName() { return this.name; }
+     * 
+ *

+ * + * @param bean + * An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object + * An object that has fields that should be used to make a + * JSONObject. + * @param names + * An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String ... names) { + this(names.length); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source + * A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName + * The ResourceBundle base name. + * @param locale + * The Locale to load the ResourceBundle for. + * @throws JSONException + * If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Constructor to specify an initial capacity of the internal map. Useful for library + * internal calls where we know, or at least can best guess, how big this JSONObject + * will be. + * + * @param initialCapacity initial capacity of the internal map. + */ + protected JSONObject(int initialCapacity){ + this.map = new HashMap(initialCapacity); + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number or if the current value associated with + * the key is not a JSONArray. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw wrongValueFormatException(key, "JSONArray", null, null); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d + * A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key + * A key string. + * @return The object associated with the key. + * @throws JSONException + * if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(key, "enum of type " + quote(clazz.getSimpleName()), null); + } + return val; + } + + /** + * Get the boolean value associated with a key. + * + * @param key + * A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(key, "Boolean", null); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + BigInteger ret = objectToBigInteger(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigInteger", object, null); + } + + /** + * Get the BigDecimal value associated with a key. If the value is float or + * double, the the {@link BigDecimal#BigDecimal(double)} constructor will + * be used. See notes on the constructor for conversion issues that may + * arise. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value + * cannot be converted to BigDecimal. + */ + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + BigDecimal ret = objectToBigDecimal(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigDecimal", object, null); + } + + /** + * Get the double value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "double", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "float", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "number", e); + } + } + + /** + * Get the int value associated with a key. + * + * @param key + * A key string. + * @return The integer value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "int", e); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(key, "JSONArray", null); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(key, "JSONObject", null); + } + + /** + * Get the long value associated with a key. + * + * @param key + * A key string. + * @return The long value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "long", e); + } + } + + /** + * Get an array of field names from a JSONObject. + * + * @param jo + * JSON object + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + /** + * Get an array of public field names from an Object. + * + * @param object + * object to read + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key + * A key string. + * @return A string which is the value. + * @throws JSONException + * if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(key, "string", null); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key + * A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1 (Integer). If there is such a property, and if it is + * an Integer, Long, Double, Float, BigInteger, or BigDecimal then add one to it. + * No overflow bounds checking is performed, so callers should initialize the key + * prior to this call with an appropriate type that can handle the maximum expected + * value. + * + * @param key + * A key string. + * @return this. + * @throws JSONException + * If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger)value).add(BigInteger.ONE)); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal)value).add(BigDecimal.ONE)); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key + * A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. Modifying this key Set will also + * modify the JSONObject. Use with caution. + * + * @see Set#iterator() + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. Modifying this key Set will also modify the + * JSONObject. Use with caution. + * + * @see Map#keySet() + * + * @return A keySet. + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get a set of entries of the JSONObject. These are raw values and may not + * match what is returned by the JSONObject get* and opt* functions. Modifying + * the returned EntrySet or the Entry objects contained therein will modify the + * backing JSONObject. This does not return a clone or a read-only view. + * + * Use with caution. + * + * @see Map#entrySet() + * + * @return An Entry Set + */ + protected Set> entrySet() { + return this.map.entrySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Removes all of the elements from this JSONObject. + * The JSONObject will be empty after this call returns. + */ + public void clear() { + this.map.clear(); + } + + /** + * Check if JSONObject is empty. + * + * @return true if JSONObject is empty, otherwise false. + */ + public boolean isEmpty() { + return this.map.isEmpty(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + if(this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + /** + * Produce a string from a Number. + * + * @param number + * A Number + * @return A String. + * @throws JSONException + * If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key + * A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key or null if not found + */ + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @param defaultValue + * The default in case the value is not found + * @return The enum value associated with the key or defaultValue + * if the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigDecimal associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. If the value + * is float or double, then the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + return objectToBigDecimal(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigDecimal conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + if (!numberIsFinite((Number)val)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + return objectToBigInteger(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigInteger conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + if (!numberIsFinite((Number)val)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key + * A key string. + * @return The value. + */ + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key + * A key string. + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(String key, float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + try { + return stringToNumber(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key + * A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + /** + * Populates the internal map of the JSONObject with the bean properties. The + * bean can not be recursive. + * + * @see JSONObject#JSONObject(Object) + * + * @param bean + * the bean + */ + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private static boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private static String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (key.length() == 0 || Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return the {@link Annotation} if the annotation exists on the current method + * or one of it's super class definitions + */ + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. Returns the depth of the + * annotation in the hierarchy. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return Depth of the annotation or -1 if the annotation is not on the method. + */ + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A boolean which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key + * A key string. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A double which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + /** + * Put a key/float pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A float which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * An int which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A long which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key + * A key string. + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONObject(value)); + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key + * key to insert into + * @param value + * value to insert + * @return this. + * @throws JSONException + * if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *

+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer string: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *
+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing + * <\/, allowing JSON text to be delivered in HTML. In JSON text, a + * string cannot contain a control character or an unescaped quote or + * backslash. + * + * @param string + * A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.isEmpty()) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key + * The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Determine if two JSONObjects are similar. + * They must contain the same set of names which must be associated with + * similar values. + * + * @param other The other JSONObject + * @return true if they are equal + */ + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + if (!this.keySet().equals(((JSONObject)other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof Number && valueOther instanceof Number) { + return isNumberSimilar((Number)valueThis, (Number)valueOther); + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + /** + * Compares two numbers to see if they are similar. + * + * If either of the numbers are Double or Float instances, then they are checked to have + * a finite value. If either value is not finite (NaN or ±infinity), then this + * function will always return false. If both numbers are finite, they are first checked + * to be the same type and implement {@link Comparable}. If they do, then the actual + * {@link Comparable#compareTo(Object)} is called. If they are not the same type, or don't + * implement Comparable, then they are converted to {@link BigDecimal}s. Finally the + * BigDecimal values are compared using {@link BigDecimal#compareTo(BigDecimal)}. + * + * @param l the Left value to compare. Can not be null. + * @param r the right value to compare. Can not be null. + * @return true if the numbers are similar, false otherwise. + */ + static boolean isNumberSimilar(Number l, Number r) { + if (!numberIsFinite(l) || !numberIsFinite(r)) { + // non-finite numbers are never similar + return false; + } + + // if the classes are the same and implement Comparable + // then use the built in compare first. + if(l.getClass().equals(r.getClass()) && l instanceof Comparable) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + int compareTo = ((Comparable)l).compareTo(r); + return compareTo==0; + } + + // BigDecimal should be able to handle all of our number types that we support through + // documentation. Convert to BigDecimal first, then use the Compare method to + // decide equality. + final BigDecimal lBigDecimal = objectToBigDecimal(l, null); + final BigDecimal rBigDecimal = objectToBigDecimal(r, null); + if (lBigDecimal == null || rBigDecimal == null) { + return false; + } + return lBigDecimal.compareTo(rBigDecimal) == 0; + } + + private static boolean numberIsFinite(Number n) { + if (n instanceof Double && (((Double) n).isInfinite() || ((Double) n).isNaN())) { + return false; + } else if (n instanceof Float && (((Float) n).isInfinite() || ((Float) n).isNaN())) { + return false; + } + return true; + } + + /** + * Tests if the value should be tried as a decimal. It makes no test if there are actual digits. + * + * @param val value to test + * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. + */ + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. + * + * @param val value to convert + * @return Number representation of the value. + * @throws NumberFormatException thrown if the value is not a valid number. A public + * caller should catch this and wrap it in a {@link JSONException} if applicable. + */ + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. can not be null. + * @return A simple JSON value. + * @throws NullPointerException + * Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o + * The object to test. + * @throws JSONException + * If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o instanceof Number && !numberIsFinite((Number) o)) { + throw new JSONException("JSON does not allow non-finite numbers."); + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names + * A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException + * If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONObject. + * + *

If

{@code indentFactor > 0}
and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *
{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{@code {
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }}
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (NULL.equals(object)) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param writer the writer object + * @return The writer. + * @throws JSONException if a called function has an error + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + if(NUMBER_PATTERN.matcher(numberAsString).matches()) { + writer.write(numberAsString); + } else { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum)value).name())); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * + *

If

{@code indentFactor > 0}
and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *
{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{@code {
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }}
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException if a called function has an error or a write error + * occurs + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newIndent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + /** + * Returns a java.util.Map containing all of the entries in this object. + * If an entry in the object is a JSONArray or JSONObject it will also + * be converted. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.Map containing the entries of this object + */ + public Map toMap() { + Map results = new HashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONObject) { + value = ((JSONObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param key name of the key + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + String key, + String valueType, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + "." + , cause); + } + + /** + * Create a new JSONException in a common format for incorrect conversions. + * @param key name of the key + * @param valueType the type of value being coerced to + * @param cause optional cause of the coercion failure + * @return JSONException that can be thrown. + */ + private static JSONException wrongValueFormatException( + String key, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")." + , cause); + } +} diff --git a/src/main/java/org/json/JSONPointer.java b/src/main/java/org/json/JSONPointer.java new file mode 100644 index 0000000..a3d1c1a --- /dev/null +++ b/src/main/java/org/json/JSONPointer.java @@ -0,0 +1,295 @@ +package org.json; + +import static java.lang.String.format; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + * + * In a nutshell, JSONPointer allows the user to navigate into a JSON document + * using strings, and retrieve targeted objects, like a simple form of XPATH. + * Path segments are separated by the '/' char, which signifies the root of + * the document when it appears as the first char of the string. Array + * elements are navigated using ordinals, counting from 0. JSONPointer strings + * may be extended to any arbitrary number of segments. If the navigation + * is successful, the matched item is returned. A matched item may be a + * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building + * fails, an appropriate exception is thrown. If the navigation fails to find + * a match, a JSONPointerException is thrown. + * + * @author JSON.org + * @version 2016-05-14 + */ +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + /** + * This class allows the user to build a JSONPointer in steps, using + * exactly one segment in each step. + */ + public static class Builder { + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + /** + * Creates a {@code JSONPointer} instance using the tokens previously set using the + * {@link #append(String)} method calls. + * @return a JSONPointer object + */ + public JSONPointer build() { + return new JSONPointer(this.refTokens); + } + + /** + * Adds an arbitrary token to the list of reference tokens. It can be any non-null value. + * + * Unlike in the case of JSON string or URI fragment representation of JSON pointers, the + * argument of this method MUST NOT be escaped. If you want to query the property called + * {@code "a~b"} then you should simply pass the {@code "a~b"} string as-is, there is no + * need to escape it as {@code "a~0b"}. + * + * @param token the new token to be appended to the list + * @return {@code this} + * @throws NullPointerException if {@code token} is null + */ + public Builder append(String token) { + if (token == null) { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; + } + + /** + * Adds an integer to the reference token list. Although not necessarily, mostly this token will + * denote an array index. + * + * @param arrayIndex the array index to be added to the token list + * @return {@code this} + */ + public Builder append(int arrayIndex) { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; + } + } + + /** + * Static factory method for {@link Builder}. Example usage: + * + *


+     * JSONPointer pointer = JSONPointer.builder()
+     *       .append("obj")
+     *       .append("other~key").append("another/key")
+     *       .append("\"")
+     *       .append(0)
+     *       .build();
+     * 
+ * + * @return a builder instance which can be used to construct a {@code JSONPointer} instance by chained + * {@link Builder#append(String)} calls. + */ + public static Builder builder() { + return new Builder(); + } + + // Segments for the JSONPointer string + private final List refTokens; + + /** + * Pre-parses and initializes a new {@code JSONPointer} instance. If you want to + * evaluate the same JSON Pointer on different JSON documents then it is recommended + * to keep the {@code JSONPointer} instances due to performance considerations. + * + * @param pointer the JSON String or URI Fragment representation of the JSON pointer. + * @throws IllegalArgumentException if {@code pointer} is not a valid JSON pointer + */ + public JSONPointer(final String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + refs = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } else if (slashIdx >= 0) { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } else { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending nulls" + //for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + //} + } + + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + /** + * @see https://tools.ietf.org/html/rfc6901#section-3 + */ + private static String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~"); + } + + /** + * Evaluates this JSON Pointer on the given {@code document}. The {@code document} + * is usually a {@link JSONObject} or a {@link JSONArray} instance, but the empty + * JSON Pointer ({@code ""}) can be evaluated on any JSON values and in such case the + * returned value will be {@code document} itself. + * + * @param document the JSON document which should be the subject of querying. + * @return the result of the evaluation + * @throws JSONPointerException if an error occurs during evaluation + */ + public Object queryFrom(Object document) throws JSONPointerException { + if (this.refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : this.refTokens) { + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(unescape(token)); + } else if (current instanceof JSONArray) { + current = readByIndexToken(current, token); + } else { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", current, + token)); + } + } + return current; + } + + /** + * Matches a JSONArray element by ordinal position + * @param current the JSONArray to be evaluated + * @param indexToken the array index in string form + * @return the matched object. If no matching item is found a + * @throws JSONPointerException is thrown if the index is out of bounds + */ + private static Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { + try { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) { + throw new JSONPointerException(format("index %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(currentArr.length()))); + } + try { + return currentArr.get(index); + } catch (JSONException e) { + throw new JSONPointerException("Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + /** + * Returns a string representing the JSONPointer path value using string + * representation + */ + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: this.refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + /** + * Escapes path segment values to an unambiguous form. + * The escape char to be inserted is '~'. The chars to be escaped + * are ~, which maps to ~0, and /, which maps to ~1. + * @param token the JSONPointer segment value to be escaped + * @return the escaped value for the token + * + * @see https://tools.ietf.org/html/rfc6901#section-3 + */ + private static String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1"); + } + + /** + * Returns a string representing the JSONPointer path value using URI + * fragment identifier representation + * @return a uri fragment string + */ + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/json/JSONPointerException.java b/src/main/java/org/json/JSONPointerException.java new file mode 100644 index 0000000..0ce1aeb --- /dev/null +++ b/src/main/java/org/json/JSONPointerException.java @@ -0,0 +1,45 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs + * during evaluating a pointer. + * + * @author JSON.org + * @version 2016-05-13 + */ +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/json/JSONPropertyIgnore.java b/src/main/java/org/json/JSONPropertyIgnore.java new file mode 100644 index 0000000..682de74 --- /dev/null +++ b/src/main/java/org/json/JSONPropertyIgnore.java @@ -0,0 +1,43 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. If this annotation is + * present at any level in the class hierarchy, then the method will + * not be serialized from the bean into the JSONObject. + */ +public @interface JSONPropertyIgnore { } diff --git a/src/main/java/org/json/JSONPropertyName.java b/src/main/java/org/json/JSONPropertyName.java new file mode 100644 index 0000000..a1bcd58 --- /dev/null +++ b/src/main/java/org/json/JSONPropertyName.java @@ -0,0 +1,47 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. A value set to empty string "" + * will have the Bean parser fall back to the default field name processing. + */ +public @interface JSONPropertyName { + /** + * @return The name of the property as to be used in the JSON Object. + */ + String value(); +} diff --git a/src/main/java/org/json/JSONString.java b/src/main/java/org/json/JSONString.java new file mode 100644 index 0000000..bcd9a81 --- /dev/null +++ b/src/main/java/org/json/JSONString.java @@ -0,0 +1,43 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * The JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/src/main/java/org/json/JSONStringer.java b/src/main/java/org/json/JSONStringer.java new file mode 100644 index 0000000..bb9e7a4 --- /dev/null +++ b/src/main/java/org/json/JSONStringer.java @@ -0,0 +1,79 @@ +package org.json; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + *

+ * A JSONStringer instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example,

+ * myString = new JSONStringer()
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject()
+ *     .toString();
which produces the string
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * @return The JSON text. + */ + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java new file mode 100644 index 0000000..e6821de --- /dev/null +++ b/src/main/java/org/json/JSONTokener.java @@ -0,0 +1,531 @@ +package org.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONTokener { + /** current read character position on the current line. */ + private long character; + /** flag to indicate if the end of the input has been found. */ + private boolean eof; + /** current read index of the input. */ + private long index; + /** current line of the input. */ + private long line; + /** previous character read from the input. */ + private char previous; + /** Reader for the input. */ + private final Reader reader; + /** flag to indicate that a previous character was requested. */ + private boolean usePrevious; + /** the number of characters read in the previous line. */ + private long characterPreviousLine; + + + /** + * Construct a JSONTokener from a Reader. The caller must close the Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.characterPreviousLine = 0; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. The caller must close the input stream. + * @param inputStream The source. + */ + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + * @throws JSONException Thrown if trying to step back more than 1 step + * or if already at the start of the string + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.decrementIndexes(); + this.usePrevious = true; + this.eof = false; + } + + /** + * Decrements the indexes for the {@link #back()} method based on the previous character read. + */ + private void decrementIndexes() { + this.index--; + if(this.previous=='\r' || this.previous == '\n') { + this.line--; + this.character=this.characterPreviousLine ; + } else if(this.character > 0){ + this.character--; + } + } + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + /** + * Checks if the end of the input has been reached. + * + * @return true if at the end of the file and we didn't step back + */ + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + * @throws JSONException thrown if there is an error stepping forward + * or backward while checking for more data. + */ + public boolean more() throws JSONException { + if(this.usePrevious) { + return true; + } + try { + this.reader.mark(1); + } catch (IOException e) { + throw new JSONException("Unable to preserve stream position", e); + } + try { + // -1 is EOF, but next() can not consume the null character '\0' + if(this.reader.read() <= 0) { + this.eof = true; + return false; + } + this.reader.reset(); + } catch (IOException e) { + throw new JSONException("Unable to read the next character from the stream", e); + } + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + * @throws JSONException Thrown if there is an error reading the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + if (c <= 0) { // End of stream + this.eof = true; + return 0; + } + this.incrementIndexes(c); + this.previous = (char) c; + return this.previous; + } + + /** + * Increments the internal indexes according to the previous character + * read and the character passed as the current character. + * @param c the current character read. + */ + private void incrementIndexes(int c) { + if(c > 0) { + this.index++; + if(c=='\r') { + this.line++; + this.characterPreviousLine = this.character; + this.character=0; + }else if (c=='\n') { + if(this.previous != '\r') { + this.line++; + this.characterPreviousLine = this.character; + } + this.character=0; + } else { + this.character++; + } + } + } + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + if(n > 0) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + throw this.syntaxError("Expected '" + c + "' and instead saw ''"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException Thrown if there is an error reading the source string. + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char)Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param delimiter A delimiter character. + * @return A string. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + if (!this.eof) { + this.back(); + } + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + * @throws JSONException Thrown if there is an error while searching + * for the to character + */ + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + // in some readers, reset() may throw an exception if + // the remaining portion of the input is greater than + // the mark size (1,000,000 above). + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return 0; + } + } while (c != to); + this.reader.mark(1); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @param causedBy The throwable that caused the error. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message, Throwable causedBy) { + return new JSONException(message + this.toString(), causedBy); + } + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/src/main/java/org/json/JSONWriter.java b/src/main/java/org/json/JSONWriter.java new file mode 100644 index 0000000..dafb1b2 --- /dev/null +++ b/src/main/java/org/json/JSONWriter.java @@ -0,0 +1,414 @@ +package org.json; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + *

+ * A JSONWriter instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example,

+ * new JSONWriter(myWriter)
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject();
which writes
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 200 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2016-08-08 + */ +public class JSONWriter { + private static final int maxdepth = 200; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: + * 'a' (array), + * 'd' (done), + * 'i' (initial), + * 'k' (key), + * 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private final JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Appendable writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + * @param w an appendable object + */ + public JSONWriter(Appendable w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + /** + * Append a value. + * @param string A string value. + * @return this + * @throws JSONException If the value is out of sequence. + */ + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + } + this.writer.append(string); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * endArray will be appended to this array. The + * endArray method must be called to mark the array's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * @param m Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * array. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * object. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * @param string A key string. + * @return this + * @throws JSONException If the key is out of place. For example, keys + * do not belong in arrays or if the key is null. + */ + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + JSONObject topObject = this.stack[this.top - 1]; + // don't use the built in putOnce method to maintain Android support + if(topObject.has(string)) { + throw new JSONException("Duplicate key \"" + string + "\""); + } + topObject.put(string, true); + if (this.comma) { + this.writer.append(','); + } + this.writer.append(JSONObject.quote(string)); + this.writer.append(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + + /** + * Begin appending a new object. All keys and values until the balancing + * endObject will be appended to this object. The + * endObject method must be called to mark the object's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + + /** + * Pop an array or object scope. + * @param c The scope to close. + * @throws JSONException If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + /** + * Push an array or object scope. + * @param jo The scope to open. + * @throws JSONException If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + String object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object != null) { + return object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex + final String numberAsString = JSONObject.numberToString((Number) value); + if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { + // Close enough to a JSON number that we will return it unquoted + return numberAsString; + } + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + return JSONObject.quote(numberAsString); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } + if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + if(value instanceof Enum){ + return JSONObject.quote(((Enum)value).name()); + } + return JSONObject.quote(value.toString()); + } + + /** + * Append either the value true or the value + * false. + * @param b A boolean. + * @return this + * @throws JSONException if a called function has an error + */ + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + /** + * Append a double value. + * @param d A double. + * @return this + * @throws JSONException If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(Double.valueOf(d)); + } + + /** + * Append a long value. + * @param l A long. + * @return this + * @throws JSONException if a called function has an error + */ + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + + + /** + * Append an object value. + * @param object The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object that implements JSONString. + * @return this + * @throws JSONException If the value is out of sequence. + */ + public JSONWriter value(Object object) throws JSONException { + return this.append(valueToString(object)); + } +} diff --git a/src/main/java/org/json/Property.java b/src/main/java/org/json/Property.java new file mode 100644 index 0000000..04e1cab --- /dev/null +++ b/src/main/java/org/json/Property.java @@ -0,0 +1,75 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Enumeration; +import java.util.Properties; + +/** + * Converts a Property file data into JSONObject and back. + * @author JSON.org + * @version 2015-05-05 + */ +public class Property { + /** + * Converts a property file object into a JSONObject. The property file object is a table of name value pairs. + * @param properties java.util.Properties + * @return JSONObject + * @throws JSONException if a called function has an error + */ + public static JSONObject toJSONObject(Properties properties) throws JSONException { + // can't use the new constructor for Android support + // JSONObject jo = new JSONObject(properties == null ? 0 : properties.size()); + JSONObject jo = new JSONObject(); + if (properties != null && !properties.isEmpty()) { + Enumeration enumProperties = properties.propertyNames(); + while(enumProperties.hasMoreElements()) { + String name = (String)enumProperties.nextElement(); + jo.put(name, properties.getProperty(name)); + } + } + return jo; + } + + /** + * Converts the JSONObject into a property file object. + * @param jo JSONObject + * @return java.util.Properties + * @throws JSONException if a called function has an error + */ + public static Properties toProperties(JSONObject jo) throws JSONException { + Properties properties = new Properties(); + if (jo != null) { + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + properties.put(key, value.toString()); + } + } + } + return properties; + } +} diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java new file mode 100644 index 0000000..805a5c3 --- /dev/null +++ b/src/main/java/org/json/XML.java @@ -0,0 +1,860 @@ +package org.json; + +/* +Copyright (c) 2015 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.Reader; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; + + +/** + * This provides static methods to convert an XML text into a JSONObject, and to + * covert a JSONObject into an XML text. + * + * @author JSON.org + * @version 2016-08-10 + */ +@SuppressWarnings("boxing") +public class XML { + + /** The Character '&'. */ + public static final Character AMP = '&'; + + /** The Character '''. */ + public static final Character APOS = '\''; + + /** The Character '!'. */ + public static final Character BANG = '!'; + + /** The Character '='. */ + public static final Character EQ = '='; + + /** The Character

{@code '>'. }
*/ + public static final Character GT = '>'; + + /** The Character '<'. */ + public static final Character LT = '<'; + + /** The Character '?'. */ + public static final Character QUEST = '?'; + + /** The Character '"'. */ + public static final Character QUOT = '"'; + + /** The Character '/'. */ + public static final Character SLASH = '/'; + + /** + * Null attribute name + */ + public static final String NULL_ATTR = "xsi:nil"; + + public static final String TYPE_ATTR = "xsi:type"; + + /** + * Creates an iterator for navigating Code Points in a string instead of + * characters. Once Java7 support is dropped, this can be replaced with + * + * string.codePoints() + * + * which is available in Java8 and above. + * + * @see http://stackoverflow.com/a/21791059/6030888 + */ + private static Iterable codePointIterator(final String string) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private int nextIndex = 0; + private int length = string.length(); + + @Override + public boolean hasNext() { + return this.nextIndex < this.length; + } + + @Override + public Integer next() { + int result = string.codePointAt(this.nextIndex); + this.nextIndex += Character.charCount(result); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** + * Replace special characters with XML escapes: + * + *
{@code 
+     * & (ampersand) is replaced by &amp;
+     * < (less than) is replaced by &lt;
+     * > (greater than) is replaced by &gt;
+     * " (double quote) is replaced by &quot;
+     * ' (single quote / apostrophe) is replaced by &apos;
+     * }
+ * + * @param string + * The string to be escaped. + * @return The escaped string. + */ + public static String escape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (final int cp : codePointIterator(string)) { + switch (cp) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + if (mustEscape(cp)) { + sb.append("&#x"); + sb.append(Integer.toHexString(cp)); + sb.append(';'); + } else { + sb.appendCodePoint(cp); + } + } + } + return sb.toString(); + } + + /** + * @param cp code point to test + * @return true if the code point is not valid for an XML + */ + private static boolean mustEscape(int cp) { + /* Valid range from https://www.w3.org/TR/REC-xml/#charsets + * + * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * + * any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. + */ + // isISOControl is true when (cp >= 0 && cp <= 0x1F) || (cp >= 0x7F && cp <= 0x9F) + // all ISO control characters are out of range except tabs and new lines + return (Character.isISOControl(cp) + && cp != 0x9 + && cp != 0xA + && cp != 0xD + ) || !( + // valid the range of acceptable characters that aren't control + (cp >= 0x20 && cp <= 0xD7FF) + || (cp >= 0xE000 && cp <= 0xFFFD) + || (cp >= 0x10000 && cp <= 0x10FFFF) + ) + ; + } + + /** + * Removes XML escapes from the string. + * + * @param string + * string to remove escapes from + * @return string with converted entities + */ + public static String unescape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (int i = 0, length = string.length(); i < length; i++) { + char c = string.charAt(i); + if (c == '&') { + final int semic = string.indexOf(';', i); + if (semic > i) { + final String entity = string.substring(i + 1, semic); + sb.append(XMLTokener.unescapeEntity(entity)); + // skip past the entity we just parsed. + i += entity.length() + 1; + } else { + // this shouldn't happen in most cases since the parser + // errors on unclosed entries. + sb.append(c); + } + } else { + // not part of an entity + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Throw an exception if the string contains whitespace. Whitespace is not + * allowed in tagNames and attributes. + * + * @param string + * A string. + * @throws JSONException Thrown if the string contains whitespace or is empty. + */ + public static void noSpace(String string) throws JSONException { + int i, length = string.length(); + if (length == 0) { + throw new JSONException("Empty string."); + } + for (i = 0; i < length; i += 1) { + if (Character.isWhitespace(string.charAt(i))) { + throw new JSONException("'" + string + + "' contains a space character."); + } + } + } + + /** + * Scan the content following the named tag, attaching it to the context. + * + * @param x + * The XMLTokener containing the source string. + * @param context + * The JSONObject that will include the new material. + * @param name + * The tag name. + * @return true if the close tag is processed. + * @throws JSONException + */ + private static boolean parse(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config) + throws JSONException { + char c; + int i; + JSONObject jsonObject = null; + String string; + String tagName; + Object token; + XMLXsiTypeConverter xmlXsiTypeConverter; + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate(config.getcDataTagName(), string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (nilAttributeFound) { + context.accumulate(tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(tagName, jsonObject); + } else { + context.accumulate(tagName, ""); + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + if(xmlXsiTypeConverter != null) { + jsonObject.accumulate(config.getcDataTagName(), + stringToValue(string, xmlXsiTypeConverter)); + } else { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepStrings() ? string : stringToValue(string)); + } + } + + } else if (token == LT) { + // Nested element + if (parse(x, jsonObject, tagName, config)) { + if (jsonObject.length() == 0) { + context.accumulate(tagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(tagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.accumulate(tagName, jsonObject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + /** + * This method tries to convert the given string value to the target object + * @param string String to convert + * @param typeConverter value converter to convert string to integer, boolean e.t.c + * @return JSON value of this string or the string + */ + public static Object stringToValue(String string, XMLXsiTypeConverter typeConverter) { + if(typeConverter != null) { + return typeConverter.convert(string); + } + return stringToValue(string); + } + + /** + * This method is the same as {@link JSONObject#stringToValue(String)}. + * + * @param string String to convert + * @return JSON value of this string or the string + */ + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. + // This method should not make calls out of the XML object. + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + /** + * direct copy of {@link JSONObject#stringToNumber(String)} to maintain Android support. + */ + private static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + /** + * direct copy of {@link JSONObject#isDecimalNotation(String)} to maintain Android support. + */ + private static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code 
+     * <[ [ ]]>}
+ * are ignored. + * + * @param string + * The source string. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(string, XMLParserConfiguration.ORIGINAL); + } + + /** + * Convert a well-formed (but not necessarily valid) XML into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code 
+     * <[ [ ]]>}
+ * are ignored. + * + * @param reader The XML source reader. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(Reader reader) throws JSONException { + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + /** + * Convert a well-formed (but not necessarily valid) XML into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code
+     * <[ [ ]]>}
+ * are ignored. + * + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document. + * + * @param reader The XML source reader. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws JSONException { + if(keepStrings) { + return toJSONObject(reader, XMLParserConfiguration.KEEP_STRINGS); + } + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + /** + * Convert a well-formed (but not necessarily valid) XML into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code
+     * <[ [ ]]>}
+ * are ignored. + * + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document. + * + * @param reader The XML source reader. + * @param config Configuration options for the parser + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration config) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(reader); + while (x.more()) { + x.skipPast("<"); + if(x.more()) { + parse(x, jo, null, config); + } + } + return jo; + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code 
+     * <[ [ ]]>}
+ * are ignored. + * + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document. + * + * @param string + * The source string. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return toJSONObject(new StringReader(string), keepStrings); + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation because + * JSON is a data format and XML is a document format. XML uses elements, + * attributes, and content text, while JSON uses unordered collections of + * name/value pairs and arrays of values. JSON does not does not like to + * distinguish between elements and attributes. Sequences of similar + * elements are represented as JSONArrays. Content text may be placed in a + * "content" member. Comments, prologs, DTDs, and
{@code 
+     * <[ [ ]]>}
+ * are ignored. + * + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to + * numbers but will instead be the exact value as seen in the XML document. + * + * @param string + * The source string. + * @param config Configuration options for the parser. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string, XMLParserConfiguration config) throws JSONException { + return toJSONObject(new StringReader(string), config); + } + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object + * A JSONObject. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toString(Object object) throws JSONException { + return toString(object, null, XMLParserConfiguration.ORIGINAL); + } + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object + * A JSONObject. + * @param tagName + * The optional name of the enclosing tag. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toString(final Object object, final String tagName) { + return toString(object, tagName, XMLParserConfiguration.ORIGINAL); + } + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object + * A JSONObject. + * @param tagName + * The optional name of the enclosing tag. + * @param config + * Configuration that can control output to XML. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toString(final Object object, final String tagName, final XMLParserConfiguration config) + throws JSONException { + StringBuilder sb = new StringBuilder(); + JSONArray ja; + JSONObject jo; + String string; + + if (object instanceof JSONObject) { + + // Emit + if (tagName != null) { + sb.append('<'); + sb.append(tagName); + sb.append('>'); + } + + // Loop thru the keys. + // don't use the new entrySet accessor to maintain Android Support + jo = (JSONObject) object; + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (value == null) { + value = ""; + } else if (value.getClass().isArray()) { + value = new JSONArray(value); + } + + // Emit content in body + if (key.equals(config.getcDataTagName())) { + if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + if (i > 0) { + sb.append('\n'); + } + Object val = ja.opt(i); + sb.append(escape(val.toString())); + } + } else { + sb.append(escape(value.toString())); + } + + // Emit an array of similar keys + + } else if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + if (val instanceof JSONArray) { + sb.append('<'); + sb.append(key); + sb.append('>'); + sb.append(toString(val, null, config)); + sb.append("'); + } else { + sb.append(toString(val, key, config)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + + // Emit a new tag + + } else { + sb.append(toString(value, key, config)); + } + } + if (tagName != null) { + + // Emit the close tag + sb.append("'); + } + return sb.toString(); + + } + + if (object != null && (object instanceof JSONArray || object.getClass().isArray())) { + if(object.getClass().isArray()) { + ja = new JSONArray(object); + } else { + ja = (JSONArray) object; + } + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + // XML does not have good support for arrays. If an array + // appears in a place where XML is lacking, synthesize an + // element. + sb.append(toString(val, tagName == null ? "array" : tagName, config)); + } + return sb.toString(); + } + + string = (object == null) ? "null" : escape(object.toString()); + return (tagName == null) ? "\"" + string + "\"" + : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName + + ">" + string + ""; + + } +} diff --git a/src/main/java/org/json/XMLParserConfiguration.java b/src/main/java/org/json/XMLParserConfiguration.java new file mode 100644 index 0000000..b9e752c --- /dev/null +++ b/src/main/java/org/json/XMLParserConfiguration.java @@ -0,0 +1,286 @@ +package org.json; +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + + +/** + * Configuration object for the XML parser. The configuration is immutable. + * @author AylwardJ + */ +@SuppressWarnings({""}) +public class XMLParserConfiguration { + /** Original Configuration of the XML Parser. */ + public static final XMLParserConfiguration ORIGINAL + = new XMLParserConfiguration(); + /** Original configuration of the XML Parser except that values are kept as strings. */ + public static final XMLParserConfiguration KEEP_STRINGS + = new XMLParserConfiguration().withKeepStrings(true); + + /** + * When parsing the XML into JSON, specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + */ + private boolean keepStrings; + + /** + * The name of the key in a JSON Object that indicates a CDATA section. Historically this has + * been the value "content" but can be changed. Use null to indicate no CDATA + * processing. + */ + private String cDataTagName; + + /** + * When parsing the XML into JSON, specifies if values with attribute xsi:nil="true" + * should be kept as attribute(false), or they should be converted to + * null(true) + */ + private boolean convertNilAttributeToNull; + + /** + * This will allow type conversion for values in XML if xsi:type attribute is defined + */ + private Map> xsiTypeMap; + + /** + * Default parser configuration. Does not keep strings (tries to implicitly convert + * values), and the CDATA Tag Name is "content". + */ + public XMLParserConfiguration () { + this.keepStrings = false; + this.cDataTagName = "content"; + this.convertNilAttributeToNull = false; + this.xsiTypeMap = Collections.emptyMap(); + } + + /** + * Configure the parser string processing and use the default CDATA Tag Name as "content". + * @param keepStrings true to parse all values as string. + * false to try and convert XML string values into a JSON value. + * @deprecated This constructor has been deprecated in favor of using the new builder + * pattern for the configuration. + * This constructor may be removed in a future release. + */ + @Deprecated + public XMLParserConfiguration (final boolean keepStrings) { + this(keepStrings, "content", false); + } + + /** + * Configure the parser string processing to try and convert XML values to JSON values and + * use the passed CDATA Tag Name the processing value. Pass null to + * disable CDATA processing + * @param cDataTagNamenull to disable CDATA processing. Any other value + * to use that value as the JSONObject key name to process as CDATA. + * @deprecated This constructor has been deprecated in favor of using the new builder + * pattern for the configuration. + * This constructor may be removed in a future release. + */ + @Deprecated + public XMLParserConfiguration (final String cDataTagName) { + this(false, cDataTagName, false); + } + + /** + * Configure the parser to use custom settings. + * @param keepStrings true to parse all values as string. + * false to try and convert XML string values into a JSON value. + * @param cDataTagNamenull to disable CDATA processing. Any other value + * to use that value as the JSONObject key name to process as CDATA. + * @deprecated This constructor has been deprecated in favor of using the new builder + * pattern for the configuration. + * This constructor may be removed in a future release. + */ + @Deprecated + public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = false; + } + + /** + * Configure the parser to use custom settings. + * @param keepStrings true to parse all values as string. + * false to try and convert XML string values into a JSON value. + * @param cDataTagName null to disable CDATA processing. Any other value + * to use that value as the JSONObject key name to process as CDATA. + * @param convertNilAttributeToNull true to parse values with attribute xsi:nil="true" as null. + * false to parse values with attribute xsi:nil="true" as {"xsi:nil":true}. + * @deprecated This constructor has been deprecated in favor of using the new builder + * pattern for the configuration. + * This constructor may be removed or marked private in a future release. + */ + @Deprecated + public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + } + + /** + * Configure the parser to use custom settings. + * @param keepStrings true to parse all values as string. + * false to try and convert XML string values into a JSON value. + * @param cDataTagName null to disable CDATA processing. Any other value + * to use that value as the JSONObject key name to process as CDATA. + * @param convertNilAttributeToNull true to parse values with attribute xsi:nil="true" as null. + * false to parse values with attribute xsi:nil="true" as {"xsi:nil":true}. + * @param xsiTypeMap new HashMap>() to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + */ + private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, + final boolean convertNilAttributeToNull, final Map> xsiTypeMap ) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); + } + + /** + * Provides a new instance of the same configuration. + */ + @Override + protected XMLParserConfiguration clone() { + // future modifications to this method should always ensure a "deep" + // clone in the case of collections. i.e. if a Map is added as a configuration + // item, a new map instance should be created and if possible each value in the + // map should be cloned as well. If the values of the map are known to also + // be immutable, then a shallow clone of the map is acceptable. + return new XMLParserConfiguration( + this.keepStrings, + this.cDataTagName, + this.convertNilAttributeToNull, + this.xsiTypeMap + ); + } + + /** + * When parsing the XML into JSON, specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @return The {@link #keepStrings} configuration value. + */ + public boolean isKeepStrings() { + return this.keepStrings; + } + + /** + * When parsing the XML into JSON, specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @param newVal + * new value to use for the {@link #keepStrings} configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withKeepStrings(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepStrings = newVal; + return newConfig; + } + + /** + * The name of the key in a JSON Object that indicates a CDATA section. Historically this has + * been the value "content" but can be changed. Use null to indicate no CDATA + * processing. + * + * @return The {@link #cDataTagName} configuration value. + */ + public String getcDataTagName() { + return this.cDataTagName; + } + + /** + * The name of the key in a JSON Object that indicates a CDATA section. Historically this has + * been the value "content" but can be changed. Use null to indicate no CDATA + * processing. + * + * @param newVal + * new value to use for the {@link #cDataTagName} configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withcDataTagName(final String newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.cDataTagName = newVal; + return newConfig; + } + + /** + * When parsing the XML into JSON, specifies if values with attribute xsi:nil="true" + * should be kept as attribute(false), or they should be converted to + * null(true) + * + * @return The {@link #convertNilAttributeToNull} configuration value. + */ + public boolean isConvertNilAttributeToNull() { + return this.convertNilAttributeToNull; + } + + /** + * When parsing the XML into JSON, specifies if values with attribute xsi:nil="true" + * should be kept as attribute(false), or they should be converted to + * null(true) + * + * @param newVal + * new value to use for the {@link #convertNilAttributeToNull} configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withConvertNilAttributeToNull(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.convertNilAttributeToNull = newVal; + return newConfig; + } + + /** + * When parsing the XML into JSON, specifies that the values with attribute xsi:type + * will be converted to target type defined to client in this configuration + * {@code Map>} to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + * @return {@link #xsiTypeMap} unmodifiable configuration map. + */ + public Map> getXsiTypeMap() { + return this.xsiTypeMap; + } + + /** + * When parsing the XML into JSON, specifies that the values with attribute xsi:type + * will be converted to target type defined to client in this configuration + * {@code Map>} to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + * @param xsiTypeMap {@code new HashMap>()} to parse values with attribute + * xsi:type="integer" as integer, xsi:type="string" as string + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLParserConfiguration withXsiTypeMap(final Map> xsiTypeMap) { + XMLParserConfiguration newConfig = this.clone(); + Map> cloneXsiTypeMap = new HashMap>(xsiTypeMap); + newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap); + return newConfig; + } +} diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java new file mode 100644 index 0000000..3bbd382 --- /dev/null +++ b/src/main/java/org/json/XMLTokener.java @@ -0,0 +1,416 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.Reader; + +/** + * The XMLTokener extends the JSONTokener to provide additional methods + * for the parsing of XML texts. + * @author JSON.org + * @version 2015-12-09 + */ +public class XMLTokener extends JSONTokener { + + + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + /** + * Construct an XMLTokener from a Reader. + * @param r A source reader. + */ + public XMLTokener(Reader r) { + super(r); + } + + /** + * Construct an XMLTokener from a string. + * @param s A source string. + */ + public XMLTokener(String s) { + super(s); + } + + /** + * Get the text in the CDATA block. + * @return The string up to the ]]>. + * @throws JSONException If the ]]> is not found. + */ + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuilder sb = new StringBuilder(); + while (more()) { + c = next(); + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && + sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + throw syntaxError("Unclosed CDATA"); + } + + + /** + * Get the next XML outer token, trimming whitespace. There are two kinds + * of tokens: the
{@code '<' }
character which begins a markup + * tag, and the content + * text between markup tags. + * + * @return A string, or a
{@code '<' }
Character, or null if + * there is no more source text. + * @throws JSONException if a called function has an error + */ + public Object nextContent() throws JSONException { + char c; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuilder(); + for (;;) { + if (c == 0) { + return sb.toString().trim(); + } + if (c == '<') { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + + + /** + *
{@code
+     * Return the next entity. These entities are translated to Characters:
+     *     &  '  >  <  ".
+     * }
+ * @param ampersand An ampersand character. + * @return A Character or an entity String if the entity is not recognized. + * @throws JSONException If missing ';' in XML entity. + */ + public Object nextEntity(@SuppressWarnings("unused") char ampersand) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String string = sb.toString(); + return unescapeEntity(string); + } + + /** + * Unescape an XML entity encoding; + * @param e entity (only the actual entity value, not the preceding & or ending ; + * @return + */ + static String unescapeEntity(String e) { + // validate + if (e == null || e.isEmpty()) { + return ""; + } + // if our entity is an encoded unicode point, parse it. + if (e.charAt(0) == '#') { + int cp; + if (e.charAt(1) == 'x' || e.charAt(1) == 'X') { + // hex encoded unicode + cp = Integer.parseInt(e.substring(2), 16); + } else { + // decimal encoded unicode + cp = Integer.parseInt(e.substring(1)); + } + return new String(new int[] {cp},0,1); + } + Character knownEntity = entity.get(e); + if(knownEntity==null) { + // we don't know the entity so keep it encoded + return '&' + e + ';'; + } + return knownEntity.toString(); + } + + + /** + *
{@code 
+     * Returns the next XML meta token. This is used for skipping over 
+     * and  structures.
+     *  }
+ * @return
{@code Syntax characters (< > / = ! ?) are returned as
+     *  Character, and strings and names are returned as Boolean. We don't care
+     *  what the values actually are.
+     *  }
+ * @throws JSONException If a string is not properly closed or if the XML + * is badly structured. + */ + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + throw syntaxError("Unterminated string"); + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + + + /** + *
{@code
+     * Get the next XML Token. These tokens are found inside of angle
+     * brackets. It may be one of these characters: / > = ! ? or it
+     * may be a string wrapped in single quotes or double quotes, or it may be a
+     * name.
+     * }
+ * @return a String or a Character. + * @throws JSONException If the XML is not well formed. + */ + public Object nextToken() throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + +// Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + +// Name + + sb = new StringBuilder(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + + + /** + * Skip characters until past the requested string. + * If it is not found, we are left at the end of the source with a result of false. + * @param to A string to skip past. + */ + // The Android implementation of JSONTokener has a public method of public void skipPast(String to) + // even though ours does not have that method, to have API compatibility, our method in the subclass + // should match. + public void skipPast(String to) { + boolean b; + char c; + int i; + int j; + int offset = 0; + int length = to.length(); + char[] circle = new char[length]; + + /* + * First fill the circle buffer with as many characters as are in the + * to string. If we reach an early end, bail. + */ + + for (i = 0; i < length; i += 1) { + c = next(); + if (c == 0) { + return; + } + circle[i] = c; + } + + /* We will loop, possibly for all of the remaining characters. */ + + for (;;) { + j = offset; + b = true; + + /* Compare the circle buffer with the to string. */ + + for (i = 0; i < length; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= length) { + j -= length; + } + } + + /* If we exit the loop with b intact, then victory is ours. */ + + if (b) { + return; + } + + /* Get the next character. If there isn't one, then defeat is ours. */ + + c = next(); + if (c == 0) { + return; + } + /* + * Shove the character in the circle buffer and advance the + * circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= length) { + offset -= length; + } + } + } +} diff --git a/src/main/java/org/json/XMLXsiTypeConverter.java b/src/main/java/org/json/XMLXsiTypeConverter.java new file mode 100644 index 0000000..0f8a8c3 --- /dev/null +++ b/src/main/java/org/json/XMLXsiTypeConverter.java @@ -0,0 +1,66 @@ +package org.json; +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Type conversion configuration interface to be used with xsi:type attributes. + *
+ * XML Sample
+ * {@code
+ *      
+ *          12345
+ *          54321
+ *      
+ * }
+ * JSON Output
+ * {@code
+ *     {
+ *         "root" : {
+ *             "asString" : "12345",
+ *             "asInt": 54321
+ *         }
+ *     }
+ * }
+ *
+ * Usage
+ * {@code
+ *      Map> xsiTypeMap = new HashMap>();
+ *      xsiTypeMap.put("string", new XMLXsiTypeConverter() {
+ *          @Override public String convert(final String value) {
+ *              return value;
+ *          }
+ *      });
+ *      xsiTypeMap.put("integer", new XMLXsiTypeConverter() {
+ *          @Override public Integer convert(final String value) {
+ *              return Integer.valueOf(value);
+ *          }
+ *      });
+ * }
+ * 
+ * @author kumar529 + * @param return type of convert method + */ +public interface XMLXsiTypeConverter { + T convert(String value); +} diff --git a/src/main/java/org/slf4j/ILoggerFactory.java b/src/main/java/org/slf4j/ILoggerFactory.java new file mode 100644 index 0000000..6fec242 --- /dev/null +++ b/src/main/java/org/slf4j/ILoggerFactory.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +/** + * ILoggerFactory instances manufacture {@link Logger} + * instances by name. + * + *

Most users retrieve {@link Logger} instances through the static + * {@link LoggerFactory#getLogger(String)} method. An instance of this + * interface is bound internally with {@link LoggerFactory} class at + * compile time. + * + * @author Ceki Gülcü + */ +public interface ILoggerFactory { + + /** + * Return an appropriate {@link Logger} instance as specified by the + * name parameter. + * + *

If the name parameter is equal to {@link Logger#ROOT_LOGGER_NAME}, that is + * the string value "ROOT" (case insensitive), then the root logger of the + * underlying logging system is returned. + * + *

Null-valued name arguments are considered invalid. + * + *

Certain extremely simple logging systems, e.g. NOP, may always + * return the same logger instance regardless of the requested name. + * + * @param name the name of the Logger to return + * @return a Logger instance + */ + public Logger getLogger(String name); +} diff --git a/src/main/java/org/slf4j/IMarkerFactory.java b/src/main/java/org/slf4j/IMarkerFactory.java new file mode 100644 index 0000000..7570221 --- /dev/null +++ b/src/main/java/org/slf4j/IMarkerFactory.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +/** + * Implementations of this interface are used to manufacture {@link Marker} + * instances. + * + *

See the section Implementing + * the SLF4J API in the FAQ for details on how to make your logging + * system conform to SLF4J. + * + * @author Ceki Gülcü + */ +public interface IMarkerFactory { + + /** + * Manufacture a {@link Marker} instance by name. If the instance has been + * created earlier, return the previously created instance. + * + *

Null name values are not allowed. + * + * @param name the name of the marker to be created, null value is + * not allowed. + * + * @return a Marker instance + */ + Marker getMarker(String name); + + /** + * Checks if the marker with the name already exists. If name is null, then false + * is returned. + * + * @param name logger name to check for + * @return true id the marker exists, false otherwise. + */ + boolean exists(String name); + + /** + * Detach an existing marker. + *

+ * Note that after a marker is detached, there might still be "dangling" references + * to the detached marker. + * + * + * @param name The name of the marker to detach + * @return whether the marker could be detached or not + */ + boolean detachMarker(String name); + + /** + * Create a marker which is detached (even at birth) from this IMarkerFactory. + * + * @param name marker name + * @return a dangling marker + * @since 1.5.1 + */ + Marker getDetachedMarker(String name); +} diff --git a/src/main/java/org/slf4j/Logger.java b/src/main/java/org/slf4j/Logger.java new file mode 100644 index 0000000..081e7f9 --- /dev/null +++ b/src/main/java/org/slf4j/Logger.java @@ -0,0 +1,881 @@ +/** + * Copyright (c) 2004-2022 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +package org.slf4j; + +import static org.slf4j.event.EventConstants.DEBUG_INT; +import static org.slf4j.event.EventConstants.ERROR_INT; +import static org.slf4j.event.EventConstants.INFO_INT; +import static org.slf4j.event.EventConstants.TRACE_INT; +import static org.slf4j.event.EventConstants.WARN_INT; +import static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.INFO; +import static org.slf4j.event.Level.TRACE; +import static org.slf4j.event.Level.WARN; + +import org.slf4j.event.Level; +import org.slf4j.helpers.CheckReturnValue; +import org.slf4j.spi.DefaultLoggingEventBuilder; +import org.slf4j.spi.LoggingEventBuilder; +import org.slf4j.spi.NOPLoggingEventBuilder; + +/** + * The org.slf4j.Logger interface is the main user entry point of SLF4J API. + * It is expected that logging takes place through concrete implementations + * of this interface. + * + *

Typical usage pattern:

+ *
+ * import org.slf4j.Logger;
+ * import org.slf4j.LoggerFactory;
+ *
+ * public class Wombat {
+ *
+ *   final static Logger logger = LoggerFactory.getLogger(Wombat.class);
+ *   Integer t;
+ *   Integer oldT;
+ *
+ *   public void setTemperature(Integer temperature) {
+ *     oldT = t;
+ *     t = temperature;
+ *     logger.debug("Temperature set to {}. Old temperature was {}.", t, oldT);
+ *     if (temperature.intValue() > 50) {
+ *       logger.info("Temperature has risen above 50 degrees.");
+ *     }
+ *   }
+ * }
+ * 
+ * + *

Note that version 2.0 of the SLF4J API introduces a fluent api, + * the most significant API change to occur in the last 20 years. + * + *

Be sure to read the FAQ entry relating to parameterized + * logging. Note that logging statements can be parameterized in + * presence of an exception/throwable. + * + *

Once you are comfortable using loggers, i.e. instances of this interface, consider using + * MDC as well as Markers. + * + * @author Ceki Gülcü + */ +public interface Logger { + + /** + * Case-insensitive String constant used to retrieve the name of the root logger. + * + * @since 1.3 + */ + final public String ROOT_LOGGER_NAME = "ROOT"; + + /** + * Return the name of this Logger instance. + * @return name of this logger instance + */ + public String getName(); + + /** + *

Make a new {@link LoggingEventBuilder} instance as appropriate for this logger implementation. + * This default implementation always returns a new instance of {@link DefaultLoggingEventBuilder}.

+ *

+ *

This method is intended to be used by logging systems implementing the SLF4J API and not + * by end users.

+ *

+ *

Also note that a {@link LoggingEventBuilder} instance should be built for all levels, + * independently of the level argument. In other words, this method is an unconditional + * constructor for the {@link LoggingEventBuilder} appropriate for this logger implementation.

+ *

+ * @param level desired level for the event builder + * @return a new {@link LoggingEventBuilder} instance as appropriate for this logger + * @since 2.0 + */ + default public LoggingEventBuilder makeLoggingEventBuilder(Level level) { + return new DefaultLoggingEventBuilder(this, level); + } + + /** + * Make a new {@link LoggingEventBuilder} instance as appropriate for this logger and the + * desired {@link Level} passed as parameter. If this Logger is disabled for the given Level, then + * a {@link NOPLoggingEventBuilder} is returned. + * + * + * @param level desired level for the event builder + * @return a new {@link LoggingEventBuilder} instance as appropriate for this logger + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atLevel(Level level) { + if (isEnabledForLevel(level)) { + return makeLoggingEventBuilder(level); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + + + + /** + * Returns whether this Logger is enabled for a given {@link Level}. + * + * @param level + * @return true if enabled, false otherwise. + */ + default public boolean isEnabledForLevel(Level level) { + int levelInt = level.toInt(); + switch (levelInt) { + case (TRACE_INT): + return isTraceEnabled(); + case (DEBUG_INT): + return isDebugEnabled(); + case (INFO_INT): + return isInfoEnabled(); + case (WARN_INT): + return isWarnEnabled(); + case (ERROR_INT): + return isErrorEnabled(); + default: + throw new IllegalArgumentException("Level [" + level + "] not recognized."); + } + } + + /** + * Is the logger instance enabled for the TRACE level? + * + * @return True if this Logger is enabled for the TRACE level, + * false otherwise. + * @since 1.4 + */ + public boolean isTraceEnabled(); + + /** + * Log a message at the TRACE level. + * + * @param msg the message string to be logged + * @since 1.4 + */ + public void trace(String msg); + + /** + * Log a message at the TRACE level according to the specified format + * and argument. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the TRACE level. + * + * @param format the format string + * @param arg the argument + * @since 1.4 + */ + public void trace(String format, Object arg); + + /** + * Log a message at the TRACE level according to the specified format + * and arguments. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the TRACE level. + * + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + * @since 1.4 + */ + public void trace(String format, Object arg1, Object arg2); + + /** + * Log a message at the TRACE level according to the specified format + * and arguments. + * + *

This form avoids superfluous string concatenation when the logger + * is disabled for the TRACE level. However, this variant incurs the hidden + * (and relatively small) cost of creating an Object[] before invoking the method, + * even if this logger is disabled for TRACE. The variants taking {@link #trace(String, Object) one} and + * {@link #trace(String, Object, Object) two} arguments exist solely in order to avoid this hidden cost. + * + * @param format the format string + * @param arguments a list of 3 or more arguments + * @since 1.4 + */ + public void trace(String format, Object... arguments); + + /** + * Log an exception (throwable) at the TRACE level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + * @since 1.4 + */ + public void trace(String msg, Throwable t); + + /** + * Similar to {@link #isTraceEnabled()} method except that the + * marker data is also taken into account. + * + * @param marker The marker data to take into consideration + * @return True if this Logger is enabled for the TRACE level, + * false otherwise. + * + * @since 1.4 + */ + public boolean isTraceEnabled(Marker marker); + + /** + * Entry point for fluent-logging for {@link Level#TRACE} level. + * + * @return LoggingEventBuilder instance as appropriate for level TRACE + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atTrace() { + if (isTraceEnabled()) { + return makeLoggingEventBuilder(TRACE); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + + /** + * Log a message with the specific Marker at the TRACE level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + * @since 1.4 + */ + public void trace(Marker marker, String msg); + + /** + * This method is similar to {@link #trace(String, Object)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg the argument + * @since 1.4 + */ + public void trace(Marker marker, String format, Object arg); + + /** + * This method is similar to {@link #trace(String, Object, Object)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + * @since 1.4 + */ + public void trace(Marker marker, String format, Object arg1, Object arg2); + + /** + * This method is similar to {@link #trace(String, Object...)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param argArray an array of arguments + * @since 1.4 + */ + public void trace(Marker marker, String format, Object... argArray); + + /** + * This method is similar to {@link #trace(String, Throwable)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + * @since 1.4 + */ + public void trace(Marker marker, String msg, Throwable t); + + /** + * Is the logger instance enabled for the DEBUG level? + * + * @return True if this Logger is enabled for the DEBUG level, + * false otherwise. + */ + public boolean isDebugEnabled(); + + /** + * Log a message at the DEBUG level. + * + * @param msg the message string to be logged + */ + public void debug(String msg); + + /** + * Log a message at the DEBUG level according to the specified format + * and argument. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the DEBUG level. + * + * @param format the format string + * @param arg the argument + */ + public void debug(String format, Object arg); + + /** + * Log a message at the DEBUG level according to the specified format + * and arguments. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the DEBUG level. + * + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void debug(String format, Object arg1, Object arg2); + + /** + * Log a message at the DEBUG level according to the specified format + * and arguments. + * + *

This form avoids superfluous string concatenation when the logger + * is disabled for the DEBUG level. However, this variant incurs the hidden + * (and relatively small) cost of creating an Object[] before invoking the method, + * even if this logger is disabled for DEBUG. The variants taking + * {@link #debug(String, Object) one} and {@link #debug(String, Object, Object) two} + * arguments exist solely in order to avoid this hidden cost. + * + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void debug(String format, Object... arguments); + + /** + * Log an exception (throwable) at the DEBUG level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void debug(String msg, Throwable t); + + /** + * Similar to {@link #isDebugEnabled()} method except that the + * marker data is also taken into account. + * + * @param marker The marker data to take into consideration + * @return True if this Logger is enabled for the DEBUG level, + * false otherwise. + */ + public boolean isDebugEnabled(Marker marker); + + /** + * Log a message with the specific Marker at the DEBUG level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + */ + public void debug(Marker marker, String msg); + + /** + * This method is similar to {@link #debug(String, Object)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg the argument + */ + public void debug(Marker marker, String format, Object arg); + + /** + * This method is similar to {@link #debug(String, Object, Object)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void debug(Marker marker, String format, Object arg1, Object arg2); + + /** + * This method is similar to {@link #debug(String, Object...)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void debug(Marker marker, String format, Object... arguments); + + /** + * This method is similar to {@link #debug(String, Throwable)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void debug(Marker marker, String msg, Throwable t); + + /** + * Entry point for fluent-logging for {@link Level#DEBUG} level. + * + * @return LoggingEventBuilder instance as appropriate for level DEBUG + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atDebug() { + if (isDebugEnabled()) { + return makeLoggingEventBuilder(DEBUG); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + + /** + * Is the logger instance enabled for the INFO level? + * + * @return True if this Logger is enabled for the INFO level, + * false otherwise. + */ + public boolean isInfoEnabled(); + + /** + * Log a message at the INFO level. + * + * @param msg the message string to be logged + */ + public void info(String msg); + + /** + * Log a message at the INFO level according to the specified format + * and argument. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the INFO level. + * + * @param format the format string + * @param arg the argument + */ + public void info(String format, Object arg); + + /** + * Log a message at the INFO level according to the specified format + * and arguments. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the INFO level. + * + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void info(String format, Object arg1, Object arg2); + + /** + * Log a message at the INFO level according to the specified format + * and arguments. + * + *

This form avoids superfluous string concatenation when the logger + * is disabled for the INFO level. However, this variant incurs the hidden + * (and relatively small) cost of creating an Object[] before invoking the method, + * even if this logger is disabled for INFO. The variants taking + * {@link #info(String, Object) one} and {@link #info(String, Object, Object) two} + * arguments exist solely in order to avoid this hidden cost. + * + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void info(String format, Object... arguments); + + /** + * Log an exception (throwable) at the INFO level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void info(String msg, Throwable t); + + /** + * Similar to {@link #isInfoEnabled()} method except that the marker + * data is also taken into consideration. + * + * @param marker The marker data to take into consideration + * @return true if this Logger is enabled for the INFO level, + * false otherwise. + */ + public boolean isInfoEnabled(Marker marker); + + /** + * Log a message with the specific Marker at the INFO level. + * + * @param marker The marker specific to this log statement + * @param msg the message string to be logged + */ + public void info(Marker marker, String msg); + + /** + * This method is similar to {@link #info(String, Object)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg the argument + */ + public void info(Marker marker, String format, Object arg); + + /** + * This method is similar to {@link #info(String, Object, Object)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void info(Marker marker, String format, Object arg1, Object arg2); + + /** + * This method is similar to {@link #info(String, Object...)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void info(Marker marker, String format, Object... arguments); + + /** + * This method is similar to {@link #info(String, Throwable)} method + * except that the marker data is also taken into consideration. + * + * @param marker the marker data for this log statement + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void info(Marker marker, String msg, Throwable t); + + /** + * Entry point for fluent-logging for {@link Level#INFO} level. + * + * @return LoggingEventBuilder instance as appropriate for level INFO + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atInfo() { + if (isInfoEnabled()) { + return makeLoggingEventBuilder(INFO); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + + /** + * Is the logger instance enabled for the WARN level? + * + * @return True if this Logger is enabled for the WARN level, + * false otherwise. + */ + public boolean isWarnEnabled(); + + /** + * Log a message at the WARN level. + * + * @param msg the message string to be logged + */ + public void warn(String msg); + + /** + * Log a message at the WARN level according to the specified format + * and argument. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the WARN level. + * + * @param format the format string + * @param arg the argument + */ + public void warn(String format, Object arg); + + /** + * Log a message at the WARN level according to the specified format + * and arguments. + * + *

This form avoids superfluous string concatenation when the logger + * is disabled for the WARN level. However, this variant incurs the hidden + * (and relatively small) cost of creating an Object[] before invoking the method, + * even if this logger is disabled for WARN. The variants taking + * {@link #warn(String, Object) one} and {@link #warn(String, Object, Object) two} + * arguments exist solely in order to avoid this hidden cost. + * + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void warn(String format, Object... arguments); + + /** + * Log a message at the WARN level according to the specified format + * and arguments. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the WARN level. + * + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void warn(String format, Object arg1, Object arg2); + + /** + * Log an exception (throwable) at the WARN level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void warn(String msg, Throwable t); + + /** + * Similar to {@link #isWarnEnabled()} method except that the marker + * data is also taken into consideration. + * + * @param marker The marker data to take into consideration + * @return True if this Logger is enabled for the WARN level, + * false otherwise. + */ + public boolean isWarnEnabled(Marker marker); + + /** + * Log a message with the specific Marker at the WARN level. + * + * @param marker The marker specific to this log statement + * @param msg the message string to be logged + */ + public void warn(Marker marker, String msg); + + /** + * This method is similar to {@link #warn(String, Object)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg the argument + */ + public void warn(Marker marker, String format, Object arg); + + /** + * This method is similar to {@link #warn(String, Object, Object)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void warn(Marker marker, String format, Object arg1, Object arg2); + + /** + * This method is similar to {@link #warn(String, Object...)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void warn(Marker marker, String format, Object... arguments); + + /** + * This method is similar to {@link #warn(String, Throwable)} method + * except that the marker data is also taken into consideration. + * + * @param marker the marker data for this log statement + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void warn(Marker marker, String msg, Throwable t); + + /** + * Entry point for fluent-logging for {@link Level#WARN} level. + * + * @return LoggingEventBuilder instance as appropriate for level WARN + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atWarn() { + if (isWarnEnabled()) { + return makeLoggingEventBuilder(WARN); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + + /** + * Is the logger instance enabled for the ERROR level? + * + * @return True if this Logger is enabled for the ERROR level, + * false otherwise. + */ + public boolean isErrorEnabled(); + + /** + * Log a message at the ERROR level. + * + * @param msg the message string to be logged + */ + public void error(String msg); + + /** + * Log a message at the ERROR level according to the specified format + * and argument. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the ERROR level. + * + * @param format the format string + * @param arg the argument + */ + public void error(String format, Object arg); + + /** + * Log a message at the ERROR level according to the specified format + * and arguments. + * + *

This form avoids superfluous object creation when the logger + * is disabled for the ERROR level. + * + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void error(String format, Object arg1, Object arg2); + + /** + * Log a message at the ERROR level according to the specified format + * and arguments. + * + *

This form avoids superfluous string concatenation when the logger + * is disabled for the ERROR level. However, this variant incurs the hidden + * (and relatively small) cost of creating an Object[] before invoking the method, + * even if this logger is disabled for ERROR. The variants taking + * {@link #error(String, Object) one} and {@link #error(String, Object, Object) two} + * arguments exist solely in order to avoid this hidden cost. + * + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void error(String format, Object... arguments); + + /** + * Log an exception (throwable) at the ERROR level with an + * accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void error(String msg, Throwable t); + + /** + * Similar to {@link #isErrorEnabled()} method except that the + * marker data is also taken into consideration. + * + * @param marker The marker data to take into consideration + * @return True if this Logger is enabled for the ERROR level, + * false otherwise. + */ + public boolean isErrorEnabled(Marker marker); + + /** + * Log a message with the specific Marker at the ERROR level. + * + * @param marker The marker specific to this log statement + * @param msg the message string to be logged + */ + public void error(Marker marker, String msg); + + /** + * This method is similar to {@link #error(String, Object)} method except that the + * marker data is also taken into consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg the argument + */ + public void error(Marker marker, String format, Object arg); + + /** + * This method is similar to {@link #error(String, Object, Object)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arg1 the first argument + * @param arg2 the second argument + */ + public void error(Marker marker, String format, Object arg1, Object arg2); + + /** + * This method is similar to {@link #error(String, Object...)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param format the format string + * @param arguments a list of 3 or more arguments + */ + public void error(Marker marker, String format, Object... arguments); + + /** + * This method is similar to {@link #error(String, Throwable)} + * method except that the marker data is also taken into + * consideration. + * + * @param marker the marker data specific to this log statement + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + public void error(Marker marker, String msg, Throwable t); + + /** + * Entry point for fluent-logging for {@link Level#ERROR} level. + * + * @return LoggingEventBuilder instance as appropriate for level ERROR + * @since 2.0 + */ + @CheckReturnValue + default public LoggingEventBuilder atError() { + if (isErrorEnabled()) { + return makeLoggingEventBuilder(ERROR); + } else { + return NOPLoggingEventBuilder.singleton(); + } + } + +} diff --git a/src/main/java/org/slf4j/LoggerFactory.java b/src/main/java/org/slf4j/LoggerFactory.java new file mode 100644 index 0000000..0dc7a72 --- /dev/null +++ b/src/main/java/org/slf4j/LoggerFactory.java @@ -0,0 +1,529 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; + +import org.slf4j.event.SubstituteLoggingEvent; +import org.slf4j.helpers.NOP_FallbackServiceProvider; +import org.slf4j.helpers.Reporter; +import org.slf4j.helpers.SubstituteLogger; +import org.slf4j.helpers.SubstituteServiceProvider; +import org.slf4j.helpers.Util; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * The LoggerFactory is a utility class producing Loggers for + * various logging APIs, e.g. logback, reload4j, log4j and JDK 1.4 logging. + * Other implementations such as {@link org.slf4j.helpers.NOPLogger NOPLogger} and + * SimpleLogger are also supported. + * + *

LoggerFactory is essentially a wrapper around an + * {@link ILoggerFactory} instance provided by a {@link SLF4JServiceProvider}. + * + *

+ * Please note that all methods in LoggerFactory are static. + * + * @author Alexander Dorokhine + * @author Robert Elliot + * @author Ceki Gülcü + * + */ +public final class LoggerFactory { + + static final String CODES_PREFIX = "https://www.slf4j.org/codes.html"; + + static final String NO_PROVIDERS_URL = CODES_PREFIX + "#noProviders"; + static final String IGNORED_BINDINGS_URL = CODES_PREFIX + "#ignoredBindings"; + + static final String MULTIPLE_BINDINGS_URL = CODES_PREFIX + "#multiple_bindings"; + static final String VERSION_MISMATCH = CODES_PREFIX + "#version_mismatch"; + static final String SUBSTITUTE_LOGGER_URL = CODES_PREFIX + "#substituteLogger"; + static final String LOGGER_NAME_MISMATCH_URL = CODES_PREFIX + "#loggerNameMismatch"; + static final String REPLAY_URL = CODES_PREFIX + "#replay"; + + static final String UNSUCCESSFUL_INIT_URL = CODES_PREFIX + "#unsuccessfulInit"; + static final String UNSUCCESSFUL_INIT_MSG = "org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. See also " + + UNSUCCESSFUL_INIT_URL; + + static final String CONNECTED_WITH_MSG = "Connected with provider of type ["; + + /** + * System property for explicitly setting the provider class. If set and the provider could be instantiated, + * then the service loading mechanism will be bypassed. + * + * @since 2.0.9 + */ + static final public String PROVIDER_PROPERTY_KEY = "slf4j.provider"; + + static final int UNINITIALIZED = 0; + static final int ONGOING_INITIALIZATION = 1; + static final int FAILED_INITIALIZATION = 2; + static final int SUCCESSFUL_INITIALIZATION = 3; + static final int NOP_FALLBACK_INITIALIZATION = 4; + + static volatile int INITIALIZATION_STATE = UNINITIALIZED; + static final SubstituteServiceProvider SUBST_PROVIDER = new SubstituteServiceProvider(); + static final NOP_FallbackServiceProvider NOP_FALLBACK_SERVICE_PROVIDER = new NOP_FallbackServiceProvider(); + + // Support for detecting mismatched logger names. + static final String DETECT_LOGGER_NAME_MISMATCH_PROPERTY = "slf4j.detectLoggerNameMismatch"; + static final String JAVA_VENDOR_PROPERTY = "java.vendor.url"; + + static boolean DETECT_LOGGER_NAME_MISMATCH = Util.safeGetBooleanSystemProperty(DETECT_LOGGER_NAME_MISMATCH_PROPERTY); + + static volatile SLF4JServiceProvider PROVIDER; + + // Package access for tests + static List findServiceProviders() { + List providerList = new ArrayList<>(); + + // retain behaviour similar to that of 1.7 series and earlier. More specifically, use the class loader that + // loaded the present class to search for services + final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader(); + + SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory); + if(explicitProvider != null) { + providerList.add(explicitProvider); + return providerList; + } + + + ServiceLoader serviceLoader = getServiceLoader(classLoaderOfLoggerFactory); + + Iterator iterator = serviceLoader.iterator(); + while (iterator.hasNext()) { + safelyInstantiate(providerList, iterator); + } + return providerList; + } + + private static ServiceLoader getServiceLoader(final ClassLoader classLoaderOfLoggerFactory) { + ServiceLoader serviceLoader; + SecurityManager securityManager = System.getSecurityManager(); + if(securityManager == null) { + serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory); + } else { + final PrivilegedAction> action = () -> ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory); + serviceLoader = AccessController.doPrivileged(action); + } + return serviceLoader; + } + + private static void safelyInstantiate(List providerList, Iterator iterator) { + try { + SLF4JServiceProvider provider = iterator.next(); + providerList.add(provider); + } catch (ServiceConfigurationError e) { + Reporter.error("A service provider failed to instantiate:\n" + e.getMessage()); + } + } + + /** + * It is LoggerFactory's responsibility to track version changes and manage + * the compatibility list. + *

+ */ + static private final String[] API_COMPATIBILITY_LIST = new String[] { "2.0" }; + + // private constructor prevents instantiation + private LoggerFactory() { + } + + /** + * Force LoggerFactory to consider itself uninitialized. + *

+ *

+ * This method is intended to be called by classes (in the same package) for + * testing purposes. This method is internal. It can be modified, renamed or + * removed at any time without notice. + *

+ *

+ * You are strongly discouraged from calling this method in production code. + */ + static void reset() { + INITIALIZATION_STATE = UNINITIALIZED; + } + + private final static void performInitialization() { + bind(); + if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { + versionSanityCheck(); + } + } + + private final static void bind() { + try { + List providersList = findServiceProviders(); + reportMultipleBindingAmbiguity(providersList); + if (providersList != null && !providersList.isEmpty()) { + PROVIDER = providersList.get(0); + earlyBindMDCAdapter(); + // SLF4JServiceProvider.initialize() is intended to be called here and nowhere else. + PROVIDER.initialize(); + INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; + reportActualBinding(providersList); + } else { + INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION; + Reporter.warn("No SLF4J providers were found."); + Reporter.warn("Defaulting to no-operation (NOP) logger implementation"); + Reporter.warn("See " + NO_PROVIDERS_URL + " for further details."); + + Set staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); + reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet); + } + postBindCleanUp(); + } catch (Exception e) { + failedBinding(e); + throw new IllegalStateException("Unexpected initialization failure", e); + } + } + + /** + * The value of PROVIDER.getMDCAdapter() can be null while PROVIDER has not yet initialized. + * + * However, SLF4JServiceProvider implementations are expected to initialize their internal + * MDCAdapter field in their constructor or on field declaration. + */ + private static void earlyBindMDCAdapter() { + MDCAdapter mdcAdapter = PROVIDER.getMDCAdapter(); + if(mdcAdapter != null) { + MDC.setMDCAdapter(mdcAdapter); + } + } + + static SLF4JServiceProvider loadExplicitlySpecified(ClassLoader classLoader) { + String explicitlySpecified = System.getProperty(PROVIDER_PROPERTY_KEY); + if (null == explicitlySpecified || explicitlySpecified.isEmpty()) { + return null; + } + try { + String message = String.format("Attempting to load provider \"%s\" specified via \"%s\" system property", explicitlySpecified, PROVIDER_PROPERTY_KEY); + Reporter.info(message); + Class clazz = classLoader.loadClass(explicitlySpecified); + Constructor constructor = clazz.getConstructor(); + Object provider = constructor.newInstance(); + return (SLF4JServiceProvider) provider; + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + String message = String.format("Failed to instantiate the specified SLF4JServiceProvider (%s)", explicitlySpecified); + Reporter.error(message, e); + return null; + } catch (ClassCastException e) { + String message = String.format("Specified SLF4JServiceProvider (%s) does not implement SLF4JServiceProvider interface", explicitlySpecified); + Reporter.error(message, e); + return null; + } + } + + private static void reportIgnoredStaticLoggerBinders(Set staticLoggerBinderPathSet) { + if (staticLoggerBinderPathSet.isEmpty()) { + return; + } + Reporter.warn("Class path contains SLF4J bindings targeting slf4j-api versions 1.7.x or earlier."); + + for (URL path : staticLoggerBinderPathSet) { + Reporter.warn("Ignoring binding found at [" + path + "]"); + } + Reporter.warn("See " + IGNORED_BINDINGS_URL + " for an explanation."); + + } + + // We need to use the name of the StaticLoggerBinder class, but we can't + // reference the class itself. + private static final String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class"; + + static Set findPossibleStaticLoggerBinderPathSet() { + // use Set instead of list in order to deal with bug #138 + // LinkedHashSet appropriate here because it preserves insertion order + // during iteration + Set staticLoggerBinderPathSet = new LinkedHashSet<>(); + try { + ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader(); + Enumeration paths; + if (loggerFactoryClassLoader == null) { + paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); + } else { + paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); + } + while (paths.hasMoreElements()) { + URL path = paths.nextElement(); + staticLoggerBinderPathSet.add(path); + } + } catch (IOException ioe) { + Reporter.error("Error getting resources from path", ioe); + } + return staticLoggerBinderPathSet; + } + + private static void postBindCleanUp() { + fixSubstituteLoggers(); + replayEvents(); + // release all resources in SUBST_FACTORY + SUBST_PROVIDER.getSubstituteLoggerFactory().clear(); + } + + private static void fixSubstituteLoggers() { + synchronized (SUBST_PROVIDER) { + SUBST_PROVIDER.getSubstituteLoggerFactory().postInitialization(); + for (SubstituteLogger substLogger : SUBST_PROVIDER.getSubstituteLoggerFactory().getLoggers()) { + Logger logger = getLogger(substLogger.getName()); + substLogger.setDelegate(logger); + } + } + + } + + static void failedBinding(Throwable t) { + INITIALIZATION_STATE = FAILED_INITIALIZATION; + Reporter.error("Failed to instantiate SLF4J LoggerFactory", t); + } + + private static void replayEvents() { + final LinkedBlockingQueue queue = SUBST_PROVIDER.getSubstituteLoggerFactory().getEventQueue(); + final int queueSize = queue.size(); + int count = 0; + final int maxDrain = 128; + List eventList = new ArrayList<>(maxDrain); + while (true) { + int numDrained = queue.drainTo(eventList, maxDrain); + if (numDrained == 0) + break; + for (SubstituteLoggingEvent event : eventList) { + replaySingleEvent(event); + if (count++ == 0) + emitReplayOrSubstituionWarning(event, queueSize); + } + eventList.clear(); + } + } + + private static void emitReplayOrSubstituionWarning(SubstituteLoggingEvent event, int queueSize) { + if (event.getLogger().isDelegateEventAware()) { + emitReplayWarning(queueSize); + } else if (event.getLogger().isDelegateNOP()) { + // nothing to do + } else { + emitSubstitutionWarning(); + } + } + + private static void replaySingleEvent(SubstituteLoggingEvent event) { + if (event == null) + return; + + SubstituteLogger substLogger = event.getLogger(); + String loggerName = substLogger.getName(); + if (substLogger.isDelegateNull()) { + throw new IllegalStateException("Delegate logger cannot be null at this state."); + } + + if (substLogger.isDelegateNOP()) { + // nothing to do + } else if (substLogger.isDelegateEventAware()) { + if(substLogger.isEnabledForLevel(event.getLevel())) { + substLogger.log(event); + } + } else { + Reporter.warn(loggerName); + } + } + + private static void emitSubstitutionWarning() { + Reporter.warn("The following set of substitute loggers may have been accessed"); + Reporter.warn("during the initialization phase. Logging calls during this"); + Reporter.warn("phase were not honored. However, subsequent logging calls to these"); + Reporter.warn("loggers will work as normally expected."); + Reporter.warn("See also " + SUBSTITUTE_LOGGER_URL); + } + + private static void emitReplayWarning(int eventCount) { + Reporter.warn("A number (" + eventCount + ") of logging calls during the initialization phase have been intercepted and are"); + Reporter.warn("now being replayed. These are subject to the filtering rules of the underlying logging system."); + Reporter.warn("See also " + REPLAY_URL); + } + + private final static void versionSanityCheck() { + try { + String requested = PROVIDER.getRequestedApiVersion(); + + boolean match = false; + for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) { + if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) { + match = true; + } + } + if (!match) { + Reporter.warn("The requested version " + requested + " by your slf4j provider is not compatible with " + + Arrays.asList(API_COMPATIBILITY_LIST).toString()); + Reporter.warn("See " + VERSION_MISMATCH + " for further details."); + } + } catch (Throwable e) { + // we should never reach here + Reporter.error("Unexpected problem occurred during version sanity check", e); + } + } + + private static boolean isAmbiguousProviderList(List providerList) { + return providerList.size() > 1; + } + + /** + * Prints a warning message on the console if multiple bindings were found + * on the class path. No reporting is done otherwise. + * + */ + private static void reportMultipleBindingAmbiguity(List providerList) { + if (isAmbiguousProviderList(providerList)) { + Reporter.warn("Class path contains multiple SLF4J providers."); + for (SLF4JServiceProvider provider : providerList) { + Reporter.warn("Found provider [" + provider + "]"); + } + Reporter.warn("See " + MULTIPLE_BINDINGS_URL + " for an explanation."); + } + } + + private static void reportActualBinding(List providerList) { + // impossible since a provider has been found + if (providerList.isEmpty()) { + throw new IllegalStateException("No providers were found which is impossible after successful initialization."); + } + + if (isAmbiguousProviderList(providerList)) { + Reporter.info("Actual provider is of type [" + providerList.get(0) + "]"); + } else { + SLF4JServiceProvider provider = providerList.get(0); + Reporter.debug(CONNECTED_WITH_MSG + provider.getClass().getName() + "]"); + } + } + + /** + * Return a logger named according to the name parameter using the + * statically bound {@link ILoggerFactory} instance. + * + * @param name + * The name of the logger. + * @return logger + */ + public static Logger getLogger(String name) { + ILoggerFactory iLoggerFactory = getILoggerFactory(); + return iLoggerFactory.getLogger(name); + } + + /** + * Return a logger named corresponding to the class passed as parameter, + * using the statically bound {@link ILoggerFactory} instance. + * + *

+ * In case the clazz parameter differs from the name of the + * caller as computed internally by SLF4J, a logger name mismatch warning + * will be printed but only if the + * slf4j.detectLoggerNameMismatch system property is set to + * true. By default, this property is not set and no warnings will be + * printed even in case of a logger name mismatch. + * + * @param clazz + * the returned logger will be named after clazz + * @return logger + * + * + * @see Detected + * logger name mismatch + */ + public static Logger getLogger(Class clazz) { + Logger logger = getLogger(clazz.getName()); + if (DETECT_LOGGER_NAME_MISMATCH) { + Class autoComputedCallingClass = Util.getCallingClass(); + if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) { + Reporter.warn(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), + autoComputedCallingClass.getName())); + Reporter.warn("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation"); + } + } + return logger; + } + + private static boolean nonMatchingClasses(Class clazz, Class autoComputedCallingClass) { + return !autoComputedCallingClass.isAssignableFrom(clazz); + } + + /** + * Return the {@link ILoggerFactory} instance in use. + *

+ *

+ * ILoggerFactory instance is bound with this class at compile time. + * + * @return the ILoggerFactory instance in use + */ + public static ILoggerFactory getILoggerFactory() { + return getProvider().getLoggerFactory(); + } + + /** + * Return the {@link SLF4JServiceProvider} in use. + + * @return provider in use + * @since 1.8.0 + */ + static SLF4JServiceProvider getProvider() { + if (INITIALIZATION_STATE == UNINITIALIZED) { + synchronized (LoggerFactory.class) { + if (INITIALIZATION_STATE == UNINITIALIZED) { + INITIALIZATION_STATE = ONGOING_INITIALIZATION; + performInitialization(); + } + } + } + switch (INITIALIZATION_STATE) { + case SUCCESSFUL_INITIALIZATION: + return PROVIDER; + case NOP_FALLBACK_INITIALIZATION: + return NOP_FALLBACK_SERVICE_PROVIDER; + case FAILED_INITIALIZATION: + throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG); + case ONGOING_INITIALIZATION: + // support re-entrant behavior. + // See also http://jira.qos.ch/browse/SLF4J-97 + return SUBST_PROVIDER; + } + throw new IllegalStateException("Unreachable code"); + } +} diff --git a/src/main/java/org/slf4j/LoggerFactoryFriend.java b/src/main/java/org/slf4j/LoggerFactoryFriend.java new file mode 100644 index 0000000..06ca201 --- /dev/null +++ b/src/main/java/org/slf4j/LoggerFactoryFriend.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2004-2021 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +/** + * All methods in this class are reserved for internal use, for testing purposes. + * + *

They can be modified, renamed or removed at any time without notice. + * + *

You are strongly discouraged calling any of the methods of this class. + * + * @since 1.8.0 + * + * @author Ceki Gülcü + */ +public class LoggerFactoryFriend { + + /* + * Force LoggerFactory to consider itself uninitialized. + */ + static public void reset() { + LoggerFactory.reset(); + } + + /** + * Set LoggerFactory.DETECT_LOGGER_NAME_MISMATCH variable. + * + * @param enabled a boolean + */ + public static void setDetectLoggerNameMismatch(boolean enabled) { + LoggerFactory.DETECT_LOGGER_NAME_MISMATCH = enabled; + } +} diff --git a/src/main/java/org/slf4j/MDC.java b/src/main/java/org/slf4j/MDC.java new file mode 100644 index 0000000..8625af3 --- /dev/null +++ b/src/main/java/org/slf4j/MDC.java @@ -0,0 +1,330 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +import java.io.Closeable; +import java.util.Deque; +import java.util.Map; + +import org.slf4j.helpers.*; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * This class hides and serves as a substitute for the underlying logging + * system's MDC implementation. + * + *

+ * If the underlying logging system offers MDC functionality, then SLF4J's MDC, + * i.e. this class, will delegate to the underlying system's MDC. Note that at + * this time, only two logging systems, namely log4j and logback, offer MDC + * functionality. For java.util.logging which does not support MDC, + * {@link BasicMDCAdapter} will be used. For other systems, i.e. slf4j-simple + * and slf4j-nop, {@link NOPMDCAdapter} will be used. + * + *

+ * Thus, as a SLF4J user, you can take advantage of MDC in the presence of log4j, + * logback, or java.util.logging, but without forcing these systems as + * dependencies upon your users. + * + *

+ * For more information on MDC please see the chapter on MDC in the + * logback manual. + * + *

+ * Please note that all methods in this class are static. + * + * @author Ceki Gülcü + * @since 1.4.1 + */ +public class MDC { + + static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA"; + private static final String MDC_APAPTER_CANNOT_BE_NULL_MESSAGE = "MDCAdapter cannot be null. See also " + NULL_MDCA_URL; + static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder"; + static MDCAdapter MDC_ADAPTER; + + /** + * An adapter to remove the key when done. + */ + public static class MDCCloseable implements Closeable { + private final String key; + + private MDCCloseable(String key) { + this.key = key; + } + + public void close() { + MDC.remove(this.key); + } + } + + private MDC() { + } + + private static MDCAdapter getMDCAdapterGivenByProvider() { + SLF4JServiceProvider provider = LoggerFactory.getProvider(); + if(provider != null) { + // If you wish to change the mdc adapter, setting the MDC.MDCAdapter variable might be insufficient. + // Keep in mind that the provider *might* perform additional internal mdcAdapter assignments that + // you would also need to replicate/adapt. + + // obtain and attach the MDCAdapter from the provider + + final MDCAdapter anAdapter = provider.getMDCAdapter(); + emitTemporaryMDCAdapterWarningIfNeeded(provider); + return anAdapter; + } else { + Reporter.error("Failed to find provider."); + Reporter.error("Defaulting to no-operation MDCAdapter implementation."); + return new NOPMDCAdapter(); + } + } + + private static void emitTemporaryMDCAdapterWarningIfNeeded(SLF4JServiceProvider provider) { + boolean isSubstitute = provider instanceof SubstituteServiceProvider; + if(isSubstitute) { + Reporter.info("Temporary mdcAdapter given by SubstituteServiceProvider."); + Reporter.info("This mdcAdapter will be replaced after backend initialization has completed."); + } + } + + /** + * Put a diagnostic context value (the val parameter) as identified with the + * key parameter into the current thread's diagnostic context map. The + * key parameter cannot be null. The val parameter + * can be null only if the underlying implementation supports it. + * + *

+ * This method delegates all work to the MDC of the underlying logging system. + * + * @param key non-null key + * @param val value to put in the map + * + * @throws IllegalArgumentException + * in case the "key" parameter is null + */ + public static void put(String key, String val) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("key parameter cannot be null"); + } + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + getMDCAdapter().put(key, val); + } + + /** + * Put a diagnostic context value (the val parameter) as identified with the + * key parameter into the current thread's diagnostic context map. The + * key parameter cannot be null. The val parameter + * can be null only if the underlying implementation supports it. + * + *

+ * This method delegates all work to the MDC of the underlying logging system. + *

+ * This method return a Closeable object who can remove key when + * close is called. + * + *

+ * Useful with Java 7 for example : + * + * try(MDC.MDCCloseable closeable = MDC.putCloseable(key, value)) { + * .... + * } + * + * + * @param key non-null key + * @param val value to put in the map + * @return a Closeable who can remove key when close + * is called. + * + * @throws IllegalArgumentException + * in case the "key" parameter is null + */ + public static MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException { + put(key, val); + return new MDCCloseable(key); + } + + /** + * Get the diagnostic context identified by the key parameter. The + * key parameter cannot be null. + * + *

+ * This method delegates all work to the MDC of the underlying logging system. + * + * @param key a key + * @return the string value identified by the key parameter. + * @throws IllegalArgumentException + * in case the "key" parameter is null + */ + public static String get(String key) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("key parameter cannot be null"); + } + + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + return getMDCAdapter().get(key); + } + + /** + * Remove the diagnostic context identified by the key parameter using + * the underlying system's MDC implementation. The key parameter + * cannot be null. This method does nothing if there is no previous value + * associated with key. + * + * @param key a key + * @throws IllegalArgumentException + * in case the "key" parameter is null + */ + public static void remove(String key) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("key parameter cannot be null"); + } + + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + getMDCAdapter().remove(key); + } + + /** + * Clear all entries in the MDC of the underlying implementation. + */ + public static void clear() { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + getMDCAdapter().clear(); + } + + /** + * Return a copy of the current thread's context map, with keys and values of + * type String. Returned value may be null. + * + * @return A copy of the current thread's context map. May be null. + * @since 1.5.1 + */ + public static Map getCopyOfContextMap() { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + return getMDCAdapter().getCopyOfContextMap(); + } + + /** + * Set the current thread's context map by first clearing any existing map and + * then copying the map passed as parameter. The context map passed as + * parameter must only contain keys and values of type String. + * + * Null valued argument is allowed (since SLF4J version 2.0.0). + * + * @param contextMap + * must contain only keys and values of type String + * @since 1.5.1 + */ + public static void setContextMap(Map contextMap) { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + getMDCAdapter().setContextMap(contextMap); + } + + /** + * Returns the MDCAdapter instance currently in use. + * + * Since 2.0.17, if the MDCAdapter instance is null, then this method set it to use + * the adapter returned by the SLF4JProvider. However, in the vast majority of cases + * the MDCAdapter will be set earlier (during initialization) by {@link LoggerFactory}. + * + * @return the MDcAdapter instance currently in use. + * @since 1.4.2 + */ + public static MDCAdapter getMDCAdapter() { + if(MDC_ADAPTER == null) { + MDC_ADAPTER = getMDCAdapterGivenByProvider(); + } + return MDC_ADAPTER; + } + + /** + * Set MDCAdapter instance to use. + * + * @since 2.0.17 + */ + static void setMDCAdapter(MDCAdapter anMDCAdapter) { + if(anMDCAdapter == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + MDC_ADAPTER = anMDCAdapter; + } + + /** + * Push a value into the deque(stack) referenced by 'key'. + * + * @param key identifies the appropriate stack + * @param value the value to push into the stack + * @since 2.0.0 + */ + static public void pushByKey(String key, String value) { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + getMDCAdapter().pushByKey(key, value); + } + + /** + * Pop the stack referenced by 'key' and return the value possibly null. + * + * @param key identifies the deque(stack) + * @return the value just popped. May be null/ + * @since 2.0.0 + */ + static public String popByKey(String key) { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + return getMDCAdapter().popByKey(key); + } + + /** + * Returns a copy of the deque(stack) referenced by 'key'. May be null. + * + * @param key identifies the stack + * @return copy of stack referenced by 'key'. May be null. + * + * @since 2.0.0 + */ + public Deque getCopyOfDequeByKey(String key) { + if (getMDCAdapter() == null) { + throw new IllegalStateException(MDC_APAPTER_CANNOT_BE_NULL_MESSAGE); + } + return getMDCAdapter().getCopyOfDequeByKey(key); + } +} diff --git a/src/main/java/org/slf4j/Marker.java b/src/main/java/org/slf4j/Marker.java new file mode 100644 index 0000000..4a6dd10 --- /dev/null +++ b/src/main/java/org/slf4j/Marker.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * Markers are named objects used to enrich log statements. Conforming logging + * system implementations of SLF4J should determine how information conveyed by + * any markers are used, if at all. Many conforming logging systems ignore marker + * data entirely. + * + *

Markers can contain references to nested markers, which in turn may + * contain references of their own. Note that the fluent API (new in 2.0) allows adding + * multiple markers to a logging statement. It is often preferable to use + * multiple markers instead of nested markers. + *

+ * + * @author Ceki Gülcü + */ +public interface Marker extends Serializable { + + /** + * This constant represents any marker, including a null marker. + */ + public final String ANY_MARKER = "*"; + + /** + * This constant represents any non-null marker. + */ + public final String ANY_NON_NULL_MARKER = "+"; + + /** + * Get the name of this Marker. + * + * @return name of marker + */ + public String getName(); + + /** + * Add a reference to another Marker. + * + *

Note that the fluent API allows adding multiple markers to a logging statement. + * It is often preferable to use multiple markers instead of nested markers. + *

+ * + * @param reference + * a reference to another marker + * @throws IllegalArgumentException + * if 'reference' is null + */ + public void add(Marker reference); + + /** + * Remove a marker reference. + * + * @param reference + * the marker reference to remove + * @return true if reference could be found and removed, false otherwise. + */ + public boolean remove(Marker reference); + + /** + * @deprecated Replaced by {@link #hasReferences()}. + */ + @Deprecated + public boolean hasChildren(); + + /** + * Does this marker have any references? + * + * @return true if this marker has one or more references, false otherwise. + */ + public boolean hasReferences(); + + /** + * Returns an Iterator which can be used to iterate over the references of this + * marker. An empty iterator is returned when this marker has no references. + * + * @return Iterator over the references of this marker + */ + public Iterator iterator(); + + /** + * Does this marker contain a reference to the 'other' marker? Marker A is defined + * to contain marker B, if A == B or if B is referenced by A, or if B is referenced + * by any one of A's references (recursively). + * + * @param other + * The marker to test for inclusion. + * @throws IllegalArgumentException + * if 'other' is null + * @return Whether this marker contains the other marker. + */ + public boolean contains(Marker other); + + /** + * Does this marker contain the marker named 'name'? + * + * If 'name' is null the returned value is always false. + * + * @param name The marker name to test for inclusion. + * @return Whether this marker contains the other marker. + */ + public boolean contains(String name); + + /** + * Markers are considered equal if they have the same name. + * + * @param o + * @return true, if this.name equals o.name + * + * @since 1.5.1 + */ + public boolean equals(Object o); + + /** + * Compute the hash code based on the name of this marker. + * Note that markers are considered equal if they have the same name. + * + * @return the computed hashCode + * @since 1.5.1 + */ + public int hashCode(); + +} diff --git a/src/main/java/org/slf4j/MarkerFactory.java b/src/main/java/org/slf4j/MarkerFactory.java new file mode 100644 index 0000000..0b15d6f --- /dev/null +++ b/src/main/java/org/slf4j/MarkerFactory.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j; + +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.helpers.Reporter; +import org.slf4j.helpers.Util; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * MarkerFactory is a utility class producing {@link Marker} instances as + * appropriate for the logging system currently in use. + * + *

+ * This class is essentially implemented as a wrapper around an + * {@link IMarkerFactory} instance bound at compile time. + * + *

+ * Please note that all methods in this class are static. + * + * @author Ceki Gülcü + */ +public class MarkerFactory { + static IMarkerFactory MARKER_FACTORY; + + private MarkerFactory() { + } + + // this is where the binding happens + static { + SLF4JServiceProvider provider = LoggerFactory.getProvider(); + if (provider != null) { + MARKER_FACTORY = provider.getMarkerFactory(); + } else { + Reporter.error("Failed to find provider"); + Reporter.error("Defaulting to BasicMarkerFactory."); + MARKER_FACTORY = new BasicMarkerFactory(); + } + } + + /** + * Return a Marker instance as specified by the name parameter using the + * previously bound {@link IMarkerFactory}instance. + * + * @param name + * The name of the {@link Marker} object to return. + * @return marker + */ + public static Marker getMarker(String name) { + return MARKER_FACTORY.getMarker(name); + } + + /** + * Create a marker which is detached (even at birth) from the MarkerFactory. + * + * @param name the name of the marker + * @return a dangling marker + * @since 1.5.1 + */ + public static Marker getDetachedMarker(String name) { + return MARKER_FACTORY.getDetachedMarker(name); + } + + /** + * Return the {@link IMarkerFactory}instance in use. + * + *

The IMarkerFactory instance is usually bound with this class at + * compile time. + * + * @return the IMarkerFactory instance in use + */ + public static IMarkerFactory getIMarkerFactory() { + return MARKER_FACTORY; + } +} \ No newline at end of file diff --git a/src/main/java/org/slf4j/event/DefaultLoggingEvent.java b/src/main/java/org/slf4j/event/DefaultLoggingEvent.java new file mode 100644 index 0000000..5a10143 --- /dev/null +++ b/src/main/java/org/slf4j/event/DefaultLoggingEvent.java @@ -0,0 +1,140 @@ +package org.slf4j.event; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.Marker; + +/** + * A default implementation of {@link LoggingEvent}. + * + * @author Ceki Gülcü + * + * @since 2.0.0 + */ +public class DefaultLoggingEvent implements LoggingEvent { + + Logger logger; + Level level; + + String message; + List markers; + List arguments; + List keyValuePairs; + + Throwable throwable; + String threadName; + long timeStamp; + + String callerBoundary; + + public DefaultLoggingEvent(Level level, Logger logger) { + this.logger = logger; + this.level = level; + } + + public void addMarker(Marker marker) { + if (markers == null) { + markers = new ArrayList<>(2); + } + markers.add(marker); + } + + @Override + public List getMarkers() { + return markers; + } + + public void addArgument(Object p) { + getNonNullArguments().add(p); + } + + public void addArguments(Object... args) { + getNonNullArguments().addAll(Arrays.asList(args)); + } + + private List getNonNullArguments() { + if (arguments == null) { + arguments = new ArrayList<>(3); + } + return arguments; + } + + @Override + public List getArguments() { + return arguments; + } + + @Override + public Object[] getArgumentArray() { + if (arguments == null) + return null; + return arguments.toArray(); + } + + public void addKeyValue(String key, Object value) { + getNonnullKeyValuePairs().add(new KeyValuePair(key, value)); + } + + private List getNonnullKeyValuePairs() { + if (keyValuePairs == null) { + keyValuePairs = new ArrayList<>(4); + } + return keyValuePairs; + } + + @Override + public List getKeyValuePairs() { + return keyValuePairs; + } + + public void setThrowable(Throwable cause) { + this.throwable = cause; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public String getLoggerName() { + return logger.getName(); + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + public String getThreadName() { + return threadName; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public void setCallerBoundary(String fqcn) { + this.callerBoundary = fqcn; + } + + public String getCallerBoundary() { + return callerBoundary; + } +} diff --git a/src/main/java/org/slf4j/event/EventConstants.java b/src/main/java/org/slf4j/event/EventConstants.java new file mode 100644 index 0000000..00975da --- /dev/null +++ b/src/main/java/org/slf4j/event/EventConstants.java @@ -0,0 +1,13 @@ +package org.slf4j.event; + +import org.slf4j.spi.LocationAwareLogger; + +public class EventConstants { + public static final int ERROR_INT = LocationAwareLogger.ERROR_INT; + public static final int WARN_INT = LocationAwareLogger.WARN_INT; + public static final int INFO_INT = LocationAwareLogger.INFO_INT; + public static final int DEBUG_INT = LocationAwareLogger.DEBUG_INT; + public static final int TRACE_INT = LocationAwareLogger.TRACE_INT; + public static final String NA_SUBST = "NA/SubstituteLogger"; + +} diff --git a/src/main/java/org/slf4j/event/EventRecordingLogger.java b/src/main/java/org/slf4j/event/EventRecordingLogger.java new file mode 100644 index 0000000..b21a2e4 --- /dev/null +++ b/src/main/java/org/slf4j/event/EventRecordingLogger.java @@ -0,0 +1,84 @@ +package org.slf4j.event; + +import java.util.Queue; + +import org.slf4j.Marker; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.helpers.SubstituteLogger; + +/** + * + * This class is used to record events during the initialization phase of the + * underlying logging framework. It is called by {@link SubstituteLogger}. + * + * + * @author Ceki Gülcü + * @author Wessel van Norel + * + */ +public class EventRecordingLogger extends LegacyAbstractLogger { + + private static final long serialVersionUID = -176083308134819629L; + + String name; + SubstituteLogger logger; + Queue eventQueue; + + // as an event recording logger we have no choice but to record all events + final static boolean RECORD_ALL_EVENTS = true; + + public EventRecordingLogger(SubstituteLogger logger, Queue eventQueue) { + this.logger = logger; + this.name = logger.getName(); + this.eventQueue = eventQueue; + } + + public String getName() { + return name; + } + + public boolean isTraceEnabled() { + return RECORD_ALL_EVENTS; + } + + public boolean isDebugEnabled() { + return RECORD_ALL_EVENTS; + } + + public boolean isInfoEnabled() { + return RECORD_ALL_EVENTS; + } + + public boolean isWarnEnabled() { + return RECORD_ALL_EVENTS; + } + + public boolean isErrorEnabled() { + return RECORD_ALL_EVENTS; + } + + // WARNING: this method assumes that any throwable is properly extracted + protected void handleNormalizedLoggingCall(Level level, Marker marker, String msg, Object[] args, Throwable throwable) { + SubstituteLoggingEvent loggingEvent = new SubstituteLoggingEvent(); + loggingEvent.setTimeStamp(System.currentTimeMillis()); + loggingEvent.setLevel(level); + loggingEvent.setLogger(logger); + loggingEvent.setLoggerName(name); + if (marker != null) { + loggingEvent.addMarker(marker); + } + loggingEvent.setMessage(msg); + loggingEvent.setThreadName(Thread.currentThread().getName()); + + loggingEvent.setArgumentArray(args); + loggingEvent.setThrowable(throwable); + + eventQueue.add(loggingEvent); + + } + + @Override + protected String getFullyQualifiedCallerName() { + return null; + } +} diff --git a/src/main/java/org/slf4j/event/KeyValuePair.java b/src/main/java/org/slf4j/event/KeyValuePair.java new file mode 100644 index 0000000..9aebe19 --- /dev/null +++ b/src/main/java/org/slf4j/event/KeyValuePair.java @@ -0,0 +1,32 @@ +package org.slf4j.event; + +import java.util.Objects; + +public class KeyValuePair { + + public final String key; + public final Object value; + + public KeyValuePair(String key, Object value) { + this.key = key; + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(key) + "=\"" + String.valueOf(value) +"\""; + } + + @Override + public boolean equals(Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + KeyValuePair that = (KeyValuePair) o; + return Objects.equals(key, that.key) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } +} diff --git a/src/main/java/org/slf4j/event/Level.java b/src/main/java/org/slf4j/event/Level.java new file mode 100644 index 0000000..a288b06 --- /dev/null +++ b/src/main/java/org/slf4j/event/Level.java @@ -0,0 +1,56 @@ +package org.slf4j.event; + +import static org.slf4j.event.EventConstants.DEBUG_INT; +import static org.slf4j.event.EventConstants.ERROR_INT; +import static org.slf4j.event.EventConstants.INFO_INT; +import static org.slf4j.event.EventConstants.TRACE_INT; +import static org.slf4j.event.EventConstants.WARN_INT; + +/** + * SLF4J's internal representation of Level. + * + * + * @author Ceki Gülcü + * @since 1.7.15 + */ +public enum Level { + + ERROR(ERROR_INT, "ERROR"), WARN(WARN_INT, "WARN"), INFO(INFO_INT, "INFO"), DEBUG(DEBUG_INT, "DEBUG"), TRACE(TRACE_INT, "TRACE"); + + private final int levelInt; + private final String levelStr; + + Level(int i, String s) { + levelInt = i; + levelStr = s; + } + + public int toInt() { + return levelInt; + } + + public static Level intToLevel(int levelInt) { + switch (levelInt) { + case (TRACE_INT): + return TRACE; + case (DEBUG_INT): + return DEBUG; + case (INFO_INT): + return INFO; + case (WARN_INT): + return WARN; + case (ERROR_INT): + return ERROR; + default: + throw new IllegalArgumentException("Level integer [" + levelInt + "] not recognized."); + } + } + + /** + * Returns the string representation of this Level. + */ + public String toString() { + return levelStr; + } + +} diff --git a/src/main/java/org/slf4j/event/LoggingEvent.java b/src/main/java/org/slf4j/event/LoggingEvent.java new file mode 100644 index 0000000..434b2d1 --- /dev/null +++ b/src/main/java/org/slf4j/event/LoggingEvent.java @@ -0,0 +1,49 @@ +package org.slf4j.event; + +import java.util.List; + +import org.slf4j.Marker; + +/** + * The minimal interface sufficient for the restitution of data passed + * by the user to the SLF4J API. + * + * @author Ceki Gülcü + * @since 1.7.15 + */ +public interface LoggingEvent { + + Level getLevel(); + + String getLoggerName(); + + String getMessage(); + + List getArguments(); + + Object[] getArgumentArray(); + + /** + * List of markers in the event, might be null. + * @return markers in the event, might be null. + */ + List getMarkers(); + + List getKeyValuePairs(); + + Throwable getThrowable(); + + long getTimeStamp(); + + String getThreadName(); + + /** + * Returns the presumed caller boundary provided by the logging library (not the user of the library). + * Null by default. + * + * @return presumed caller, null by default. + */ + default String getCallerBoundary() { + return null; + } +} diff --git a/src/main/java/org/slf4j/event/SubstituteLoggingEvent.java b/src/main/java/org/slf4j/event/SubstituteLoggingEvent.java new file mode 100644 index 0000000..4c8f7ad --- /dev/null +++ b/src/main/java/org/slf4j/event/SubstituteLoggingEvent.java @@ -0,0 +1,115 @@ +package org.slf4j.event; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Marker; +import org.slf4j.helpers.SubstituteLogger; + +public class SubstituteLoggingEvent implements LoggingEvent { + + Level level; + List markers; + String loggerName; + SubstituteLogger logger; + String threadName; + String message; + Object[] argArray; + List keyValuePairList; + + long timeStamp; + Throwable throwable; + + public Level getLevel() { + return level; + } + + public void setLevel(Level level) { + this.level = level; + } + + public List getMarkers() { + return markers; + } + + public void addMarker(Marker marker) { + if (marker == null) + return; + + if (markers == null) { + markers = new ArrayList<>(2); + } + + markers.add(marker); + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public SubstituteLogger getLogger() { + return logger; + } + + public void setLogger(SubstituteLogger logger) { + this.logger = logger; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object[] getArgumentArray() { + return argArray; + } + + public void setArgumentArray(Object[] argArray) { + this.argArray = argArray; + } + + @Override + public List getArguments() { + if (argArray == null) { + return null; + } + return Arrays.asList(argArray); + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public String getThreadName() { + return threadName; + } + + public void setThreadName(String threadName) { + this.threadName = threadName; + } + + public Throwable getThrowable() { + return throwable; + } + + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public List getKeyValuePairs() { + return keyValuePairList; + } +} diff --git a/src/main/java/org/slf4j/helpers/AbstractLogger.java b/src/main/java/org/slf4j/helpers/AbstractLogger.java new file mode 100644 index 0000000..4ed9f92 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/AbstractLogger.java @@ -0,0 +1,423 @@ +/** + * Copyright (c) 2004-2019 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.event.Level; + +/** + * An abstract implementation which delegates actual logging work to the + * {@link #handleNormalizedLoggingCall(Level, Marker, String, Object[], Throwable)} method. + * + * @author Ceki Gülcü + * @since 2.0 + */ +public abstract class AbstractLogger implements Logger, Serializable { + + private static final long serialVersionUID = -2529255052481744503L; + + protected String name; + + public String getName() { + return name; + } + + /** + * Replace this instance with a homonymous (same name) logger returned + * by LoggerFactory. Note that this method is only called during + * deserialization. + * + *

+ * This approach will work well if the desired ILoggerFactory is the one + * referenced by {@link LoggerFactory} However, if the user manages its logger hierarchy + * through a different (non-static) mechanism, e.g. dependency injection, then + * this approach would be mostly counterproductive. + * + * @return logger with same name as returned by LoggerFactory + * @throws ObjectStreamException + */ + protected Object readResolve() throws ObjectStreamException { + // using getName() instead of this.name works even for + // NOPLogger + return LoggerFactory.getLogger(getName()); + } + + @Override + public void trace(String msg) { + if (isTraceEnabled()) { + handle_0ArgsCall(Level.TRACE, null, msg, null); + } + } + + @Override + public void trace(String format, Object arg) { + if (isTraceEnabled()) { + handle_1ArgsCall(Level.TRACE, null, format, arg); + } + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + if (isTraceEnabled()) { + handle2ArgsCall(Level.TRACE, null, format, arg1, arg2); + } + } + + @Override + public void trace(String format, Object... arguments) { + if (isTraceEnabled()) { + handleArgArrayCall(Level.TRACE, null, format, arguments); + } + } + + @Override + public void trace(String msg, Throwable t) { + if (isTraceEnabled()) { + handle_0ArgsCall(Level.TRACE, null, msg, t); + } + } + + @Override + public void trace(Marker marker, String msg) { + if (isTraceEnabled(marker)) { + handle_0ArgsCall(Level.TRACE, marker, msg, null); + } + } + + @Override + public void trace(Marker marker, String format, Object arg) { + if (isTraceEnabled(marker)) { + handle_1ArgsCall(Level.TRACE, marker, format, arg); + } + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + if (isTraceEnabled(marker)) { + handle2ArgsCall(Level.TRACE, marker, format, arg1, arg2); + } + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + if (isTraceEnabled(marker)) { + handleArgArrayCall(Level.TRACE, marker, format, argArray); + } + } + + public void trace(Marker marker, String msg, Throwable t) { + if (isTraceEnabled(marker)) { + handle_0ArgsCall(Level.TRACE, marker, msg, t); + } + } + + public void debug(String msg) { + if (isDebugEnabled()) { + handle_0ArgsCall(Level.DEBUG, null, msg, null); + } + } + + public void debug(String format, Object arg) { + if (isDebugEnabled()) { + handle_1ArgsCall(Level.DEBUG, null, format, arg); + } + } + + public void debug(String format, Object arg1, Object arg2) { + if (isDebugEnabled()) { + handle2ArgsCall(Level.DEBUG, null, format, arg1, arg2); + } + } + + public void debug(String format, Object... arguments) { + if (isDebugEnabled()) { + handleArgArrayCall(Level.DEBUG, null, format, arguments); + } + } + + public void debug(String msg, Throwable t) { + if (isDebugEnabled()) { + handle_0ArgsCall(Level.DEBUG, null, msg, t); + } + } + + public void debug(Marker marker, String msg) { + if (isDebugEnabled(marker)) { + handle_0ArgsCall(Level.DEBUG, marker, msg, null); + } + } + + public void debug(Marker marker, String format, Object arg) { + if (isDebugEnabled(marker)) { + handle_1ArgsCall(Level.DEBUG, marker, format, arg); + } + } + + public void debug(Marker marker, String format, Object arg1, Object arg2) { + if (isDebugEnabled(marker)) { + handle2ArgsCall(Level.DEBUG, marker, format, arg1, arg2); + } + } + + public void debug(Marker marker, String format, Object... arguments) { + if (isDebugEnabled(marker)) { + handleArgArrayCall(Level.DEBUG, marker, format, arguments); + } + } + + public void debug(Marker marker, String msg, Throwable t) { + if (isDebugEnabled(marker)) { + handle_0ArgsCall(Level.DEBUG, marker, msg, t); + } + } + + public void info(String msg) { + if (isInfoEnabled()) { + handle_0ArgsCall(Level.INFO, null, msg, null); + } + } + + public void info(String format, Object arg) { + if (isInfoEnabled()) { + handle_1ArgsCall(Level.INFO, null, format, arg); + } + } + + public void info(String format, Object arg1, Object arg2) { + if (isInfoEnabled()) { + handle2ArgsCall(Level.INFO, null, format, arg1, arg2); + } + } + + public void info(String format, Object... arguments) { + if (isInfoEnabled()) { + handleArgArrayCall(Level.INFO, null, format, arguments); + } + } + + public void info(String msg, Throwable t) { + if (isInfoEnabled()) { + handle_0ArgsCall(Level.INFO, null, msg, t); + } + } + + public void info(Marker marker, String msg) { + if (isInfoEnabled(marker)) { + handle_0ArgsCall(Level.INFO, marker, msg, null); + } + } + + public void info(Marker marker, String format, Object arg) { + if (isInfoEnabled(marker)) { + handle_1ArgsCall(Level.INFO, marker, format, arg); + } + } + + public void info(Marker marker, String format, Object arg1, Object arg2) { + if (isInfoEnabled(marker)) { + handle2ArgsCall(Level.INFO, marker, format, arg1, arg2); + } + } + + public void info(Marker marker, String format, Object... arguments) { + if (isInfoEnabled(marker)) { + handleArgArrayCall(Level.INFO, marker, format, arguments); + } + } + + public void info(Marker marker, String msg, Throwable t) { + if (isInfoEnabled(marker)) { + handle_0ArgsCall(Level.INFO, marker, msg, t); + } + } + + public void warn(String msg) { + if (isWarnEnabled()) { + handle_0ArgsCall(Level.WARN, null, msg, null); + } + } + + public void warn(String format, Object arg) { + if (isWarnEnabled()) { + handle_1ArgsCall(Level.WARN, null, format, arg); + } + } + + public void warn(String format, Object arg1, Object arg2) { + if (isWarnEnabled()) { + handle2ArgsCall(Level.WARN, null, format, arg1, arg2); + } + } + + public void warn(String format, Object... arguments) { + if (isWarnEnabled()) { + handleArgArrayCall(Level.WARN, null, format, arguments); + } + } + + public void warn(String msg, Throwable t) { + if (isWarnEnabled()) { + handle_0ArgsCall(Level.WARN, null, msg, t); + } + } + + public void warn(Marker marker, String msg) { + if (isWarnEnabled(marker)) { + handle_0ArgsCall(Level.WARN, marker, msg, null); + } + } + + public void warn(Marker marker, String format, Object arg) { + if (isWarnEnabled(marker)) { + handle_1ArgsCall(Level.WARN, marker, format, arg); + } + } + + public void warn(Marker marker, String format, Object arg1, Object arg2) { + if (isWarnEnabled(marker)) { + handle2ArgsCall(Level.WARN, marker, format, arg1, arg2); + } + } + + public void warn(Marker marker, String format, Object... arguments) { + if (isWarnEnabled(marker)) { + handleArgArrayCall(Level.WARN, marker, format, arguments); + } + } + + public void warn(Marker marker, String msg, Throwable t) { + if (isWarnEnabled(marker)) { + handle_0ArgsCall(Level.WARN, marker, msg, t); + } + } + + public void error(String msg) { + if (isErrorEnabled()) { + handle_0ArgsCall(Level.ERROR, null, msg, null); + } + } + + public void error(String format, Object arg) { + if (isErrorEnabled()) { + handle_1ArgsCall(Level.ERROR, null, format, arg); + } + } + + public void error(String format, Object arg1, Object arg2) { + if (isErrorEnabled()) { + handle2ArgsCall(Level.ERROR, null, format, arg1, arg2); + } + } + + public void error(String format, Object... arguments) { + if (isErrorEnabled()) { + handleArgArrayCall(Level.ERROR, null, format, arguments); + } + } + + public void error(String msg, Throwable t) { + if (isErrorEnabled()) { + handle_0ArgsCall(Level.ERROR, null, msg, t); + } + } + + public void error(Marker marker, String msg) { + if (isErrorEnabled(marker)) { + handle_0ArgsCall(Level.ERROR, marker, msg, null); + } + } + + public void error(Marker marker, String format, Object arg) { + if (isErrorEnabled(marker)) { + handle_1ArgsCall(Level.ERROR, marker, format, arg); + } + } + + public void error(Marker marker, String format, Object arg1, Object arg2) { + if (isErrorEnabled(marker)) { + handle2ArgsCall(Level.ERROR, marker, format, arg1, arg2); + } + } + + public void error(Marker marker, String format, Object... arguments) { + if (isErrorEnabled(marker)) { + handleArgArrayCall(Level.ERROR, marker, format, arguments); + } + } + + public void error(Marker marker, String msg, Throwable t) { + if (isErrorEnabled(marker)) { + handle_0ArgsCall(Level.ERROR, marker, msg, t); + } + } + + private void handle_0ArgsCall(Level level, Marker marker, String msg, Throwable t) { + handleNormalizedLoggingCall(level, marker, msg, null, t); + } + + private void handle_1ArgsCall(Level level, Marker marker, String msg, Object arg1) { + handleNormalizedLoggingCall(level, marker, msg, new Object[] { arg1 }, null); + } + + private void handle2ArgsCall(Level level, Marker marker, String msg, Object arg1, Object arg2) { + if (arg2 instanceof Throwable) { + handleNormalizedLoggingCall(level, marker, msg, new Object[] { arg1 }, (Throwable) arg2); + } else { + handleNormalizedLoggingCall(level, marker, msg, new Object[] { arg1, arg2 }, null); + } + } + + private void handleArgArrayCall(Level level, Marker marker, String msg, Object[] args) { + Throwable throwableCandidate = MessageFormatter.getThrowableCandidate(args); + if (throwableCandidate != null) { + Object[] trimmedCopy = MessageFormatter.trimmedCopy(args); + handleNormalizedLoggingCall(level, marker, msg, trimmedCopy, throwableCandidate); + } else { + handleNormalizedLoggingCall(level, marker, msg, args, null); + } + } + + abstract protected String getFullyQualifiedCallerName(); + + /** + * Given various arguments passed as parameters, perform actual logging. + * + *

This method assumes that the separation of the args array into actual + * objects and a throwable has been already operated. + * + * @param level the SLF4J level for this event + * @param marker The marker to be used for this event, may be null. + * @param messagePattern The message pattern which will be parsed and formatted + * @param arguments the array of arguments to be formatted, may be null + * @param throwable The exception whose stack trace should be logged, may be null + */ + abstract protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable); + +} diff --git a/src/main/java/org/slf4j/helpers/BasicMDCAdapter.java b/src/main/java/org/slf4j/helpers/BasicMDCAdapter.java new file mode 100644 index 0000000..95cb904 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/BasicMDCAdapter.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import org.slf4j.spi.MDCAdapter; + +import java.util.*; + +/** + * Basic MDC implementation, which can be used with logging systems that lack + * out-of-the-box MDC support. + * + * This code was initially inspired by logback's LogbackMDCAdapter. However, + * LogbackMDCAdapter has evolved and is now considerably more sophisticated. + * + * @author Ceki Gulcu + * @author Maarten Bosteels + * @author Lukasz Cwik + * + * @since 1.5.0 + */ +public class BasicMDCAdapter implements MDCAdapter { + + private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks(); + + private final InheritableThreadLocal> inheritableThreadLocalMap = new InheritableThreadLocal>() { + @Override + protected Map childValue(Map parentValue) { + if (parentValue == null) { + return null; + } + return new HashMap<>(parentValue); + } + }; + + /** + * Put a context value (the val parameter) as identified with + * the key parameter into the current thread's context map. + * Note that contrary to log4j, the val parameter can be null. + * + *

+ * If the current thread does not have a context map it is created as a side + * effect of this call. + * + * @throws IllegalArgumentException + * in case the "key" parameter is null + */ + public void put(String key, String val) { + if (key == null) { + throw new IllegalArgumentException("key cannot be null"); + } + Map map = inheritableThreadLocalMap.get(); + if (map == null) { + map = new HashMap<>(); + inheritableThreadLocalMap.set(map); + } + map.put(key, val); + } + + /** + * Get the context identified by the key parameter. + */ + public String get(String key) { + Map map = inheritableThreadLocalMap.get(); + if ((map != null) && (key != null)) { + return map.get(key); + } else { + return null; + } + } + + /** + * Remove the context identified by the key parameter. + */ + public void remove(String key) { + Map map = inheritableThreadLocalMap.get(); + if (map != null) { + map.remove(key); + } + } + + /** + * Clear all entries in the MDC. + */ + public void clear() { + Map map = inheritableThreadLocalMap.get(); + if (map != null) { + map.clear(); + inheritableThreadLocalMap.remove(); + } + } + + /** + * Returns the keys in the MDC as a {@link Set} of {@link String}s The + * returned value can be null. + * + * @return the keys in the MDC + */ + public Set getKeys() { + Map map = inheritableThreadLocalMap.get(); + if (map != null) { + return map.keySet(); + } else { + return null; + } + } + + /** + * Return a copy of the current thread's context map. + * Returned value may be null. + * + */ + public Map getCopyOfContextMap() { + Map oldMap = inheritableThreadLocalMap.get(); + if (oldMap != null) { + return new HashMap<>(oldMap); + } else { + return null; + } + } + + public void setContextMap(Map contextMap) { + Map copy = null; + if (contextMap != null) { + copy = new HashMap<>(contextMap); + } + inheritableThreadLocalMap.set(copy); + } + + @Override + public void pushByKey(String key, String value) { + threadLocalMapOfDeques.pushByKey(key, value); + } + + @Override + public String popByKey(String key) { + return threadLocalMapOfDeques.popByKey(key); + } + + @Override + public Deque getCopyOfDequeByKey(String key) { + return threadLocalMapOfDeques.getCopyOfDequeByKey(key); + } + @Override + public void clearDequeByKey(String key) { + threadLocalMapOfDeques.clearDequeByKey(key); + } +} diff --git a/src/main/java/org/slf4j/helpers/BasicMarker.java b/src/main/java/org/slf4j/helpers/BasicMarker.java new file mode 100644 index 0000000..9e3a6ae --- /dev/null +++ b/src/main/java/org/slf4j/helpers/BasicMarker.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Marker; + +/** + * A simple implementation of the {@link Marker} interface. + * + * @author Ceki Gülcü + * @author Joern Huxhorn + */ +public class BasicMarker implements Marker { + + private static final long serialVersionUID = -2849567615646933777L; + private final String name; + private final List referenceList = new CopyOnWriteArrayList<>(); + + BasicMarker(String name) { + if (name == null) { + throw new IllegalArgumentException("A marker name cannot be null"); + } + this.name = name; + } + + public String getName() { + return name; + } + + public void add(Marker reference) { + if (reference == null) { + throw new IllegalArgumentException("A null value cannot be added to a Marker as reference."); + } + + // no point in adding the reference multiple times + if (this.contains(reference)) { + return; + + } else if (reference.contains(this)) { // avoid recursion + // a potential reference should not hold its future "parent" as a reference + return; + } else { + referenceList.add(reference); + } + } + + public boolean hasReferences() { + return (referenceList.size() > 0); + } + + @Deprecated + public boolean hasChildren() { + return hasReferences(); + } + + public Iterator iterator() { + return referenceList.iterator(); + } + + public boolean remove(Marker referenceToRemove) { + return referenceList.remove(referenceToRemove); + } + + public boolean contains(Marker other) { + if (other == null) { + throw new IllegalArgumentException("Other cannot be null"); + } + + if (this.equals(other)) { + return true; + } + + if (hasReferences()) { + for (Marker ref : referenceList) { + if (ref.contains(other)) { + return true; + } + } + } + return false; + } + + /** + * This method is mainly used with Expression Evaluators. + */ + public boolean contains(String name) { + if (name == null) { + throw new IllegalArgumentException("Other cannot be null"); + } + + if (this.name.equals(name)) { + return true; + } + + if (hasReferences()) { + for (Marker ref : referenceList) { + if (ref.contains(name)) { + return true; + } + } + } + return false; + } + + private static final String OPEN = "[ "; + private static final String CLOSE = " ]"; + private static final String SEP = ", "; + + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Marker)) + return false; + + final Marker other = (Marker) obj; + return name.equals(other.getName()); + } + + public int hashCode() { + return name.hashCode(); + } + + public String toString() { + if (!this.hasReferences()) { + return this.getName(); + } + Iterator it = this.iterator(); + Marker reference; + StringBuilder sb = new StringBuilder(this.getName()); + sb.append(' ').append(OPEN); + while (it.hasNext()) { + reference = it.next(); + sb.append(reference.getName()); + if (it.hasNext()) { + sb.append(SEP); + } + } + sb.append(CLOSE); + + return sb.toString(); + } +} diff --git a/src/main/java/org/slf4j/helpers/BasicMarkerFactory.java b/src/main/java/org/slf4j/helpers/BasicMarkerFactory.java new file mode 100644 index 0000000..06b0ec5 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/BasicMarkerFactory.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.slf4j.IMarkerFactory; +import org.slf4j.Marker; + +/** + * An almost trivial implementation of the {@link IMarkerFactory} + * interface which creates {@link BasicMarker} instances. + * + *

Simple logging systems can conform to the SLF4J API by binding + * {@link org.slf4j.MarkerFactory} with an instance of this class. + * + * @author Ceki Gülcü + */ +public class BasicMarkerFactory implements IMarkerFactory { + + private final ConcurrentMap markerMap = new ConcurrentHashMap<>(); + + /** + * Regular users should not create + * BasicMarkerFactory instances. Marker + * instances can be obtained using the static {@link + * org.slf4j.MarkerFactory#getMarker} method. + */ + public BasicMarkerFactory() { + } + + /** + * Manufacture a {@link BasicMarker} instance by name. If the instance has been + * created earlier, return the previously created instance. + * + * @param name the name of the marker to be created + * @return a Marker instance + */ + public Marker getMarker(String name) { + if (name == null) { + throw new IllegalArgumentException("Marker name cannot be null"); + } + + Marker marker = markerMap.get(name); + if (marker == null) { + marker = new BasicMarker(name); + Marker oldMarker = markerMap.putIfAbsent(name, marker); + if (oldMarker != null) { + marker = oldMarker; + } + } + return marker; + } + + /** + * Does the name marked already exist? + */ + public boolean exists(String name) { + if (name == null) { + return false; + } + return markerMap.containsKey(name); + } + + public boolean detachMarker(String name) { + if (name == null) { + return false; + } + return (markerMap.remove(name) != null); + } + + public Marker getDetachedMarker(String name) { + return new BasicMarker(name); + } + +} diff --git a/src/main/java/org/slf4j/helpers/CheckReturnValue.java b/src/main/java/org/slf4j/helpers/CheckReturnValue.java new file mode 100644 index 0000000..e1c9803 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/CheckReturnValue.java @@ -0,0 +1,30 @@ +package org.slf4j.helpers; + + +import org.slf4j.Marker; + +import java.lang.annotation.*; + +/** + *

Used to annotate methods in the {@link org.slf4j.spi.LoggingEventBuilder} interface + * which return an instance of LoggingEventBuilder (usually as this). Such + * methods should be followed by one of the terminating log() methods returning + * void.

+ *

+ *

IntelliJ IDEA supports a check for annotations named as CheckReturnValue + * regardless of the containing package. For more information on this feature in IntelliJ IDEA, + * select File → Setting → Editor → Inspections and then Java → Probable Bugs → + * Result of method call ignored.

+ *

+ * + *

As for Eclipse, this feature has been requested in + * bug 572496

+ * + * @author Ceki Gülcü + * @since 2.0.0-beta1 + */ +@Documented +@Target( { ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckReturnValue { +} diff --git a/src/main/java/org/slf4j/helpers/FormattingTuple.java b/src/main/java/org/slf4j/helpers/FormattingTuple.java new file mode 100644 index 0000000..a416e1d --- /dev/null +++ b/src/main/java/org/slf4j/helpers/FormattingTuple.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +/** + * Holds the results of formatting done by {@link MessageFormatter}. + * + * @author Joern Huxhorn + */ +public class FormattingTuple { + + static public FormattingTuple NULL = new FormattingTuple(null); + + private final String message; + private final Throwable throwable; + private final Object[] argArray; + + public FormattingTuple(String message) { + this(message, null, null); + } + + public FormattingTuple(String message, Object[] argArray, Throwable throwable) { + this.message = message; + this.throwable = throwable; + this.argArray = argArray; + } + + public String getMessage() { + return message; + } + + public Object[] getArgArray() { + return argArray; + } + + public Throwable getThrowable() { + return throwable; + } + +} diff --git a/src/main/java/org/slf4j/helpers/LegacyAbstractLogger.java b/src/main/java/org/slf4j/helpers/LegacyAbstractLogger.java new file mode 100644 index 0000000..dcc9000 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/LegacyAbstractLogger.java @@ -0,0 +1,39 @@ +package org.slf4j.helpers; + +import org.slf4j.Marker; + +/** + * Provides minimal default implementations for {@link #isTraceEnabled(Marker)}, {@link #isDebugEnabled(Marker)} and other similar methods. + * + * @since 2.0 + */ +abstract public class LegacyAbstractLogger extends AbstractLogger { + + private static final long serialVersionUID = -7041884104854048950L; + + @Override + public boolean isTraceEnabled(Marker marker) { + return isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return isErrorEnabled(); + } + +} diff --git a/src/main/java/org/slf4j/helpers/MarkerIgnoringBase.java b/src/main/java/org/slf4j/helpers/MarkerIgnoringBase.java new file mode 100644 index 0000000..715cec7 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/MarkerIgnoringBase.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import org.slf4j.Logger; +import org.slf4j.Marker; + +/** + * This class serves as base for adapters or native implementations of logging systems + * lacking Marker support. In this implementation, methods taking marker data + * simply invoke the corresponding method without the Marker argument, discarding + * any marker data passed as argument. + * + * @author Ceki Gulcu + * @deprecated + */ +public abstract class MarkerIgnoringBase extends NamedLoggerBase implements Logger { + + private static final long serialVersionUID = 9044267456635152283L; + + public boolean isTraceEnabled(Marker marker) { + return isTraceEnabled(); + } + + public void trace(Marker marker, String msg) { + trace(msg); + } + + public void trace(Marker marker, String format, Object arg) { + trace(format, arg); + } + + public void trace(Marker marker, String format, Object arg1, Object arg2) { + trace(format, arg1, arg2); + } + + public void trace(Marker marker, String format, Object... arguments) { + trace(format, arguments); + } + + public void trace(Marker marker, String msg, Throwable t) { + trace(msg, t); + } + + public boolean isDebugEnabled(Marker marker) { + return isDebugEnabled(); + } + + public void debug(Marker marker, String msg) { + debug(msg); + } + + public void debug(Marker marker, String format, Object arg) { + debug(format, arg); + } + + public void debug(Marker marker, String format, Object arg1, Object arg2) { + debug(format, arg1, arg2); + } + + public void debug(Marker marker, String format, Object... arguments) { + debug(format, arguments); + } + + public void debug(Marker marker, String msg, Throwable t) { + debug(msg, t); + } + + public boolean isInfoEnabled(Marker marker) { + return isInfoEnabled(); + } + + public void info(Marker marker, String msg) { + info(msg); + } + + public void info(Marker marker, String format, Object arg) { + info(format, arg); + } + + public void info(Marker marker, String format, Object arg1, Object arg2) { + info(format, arg1, arg2); + } + + public void info(Marker marker, String format, Object... arguments) { + info(format, arguments); + } + + public void info(Marker marker, String msg, Throwable t) { + info(msg, t); + } + + public boolean isWarnEnabled(Marker marker) { + return isWarnEnabled(); + } + + public void warn(Marker marker, String msg) { + warn(msg); + } + + public void warn(Marker marker, String format, Object arg) { + warn(format, arg); + } + + public void warn(Marker marker, String format, Object arg1, Object arg2) { + warn(format, arg1, arg2); + } + + public void warn(Marker marker, String format, Object... arguments) { + warn(format, arguments); + } + + public void warn(Marker marker, String msg, Throwable t) { + warn(msg, t); + } + + public boolean isErrorEnabled(Marker marker) { + return isErrorEnabled(); + } + + public void error(Marker marker, String msg) { + error(msg); + } + + public void error(Marker marker, String format, Object arg) { + error(format, arg); + } + + public void error(Marker marker, String format, Object arg1, Object arg2) { + error(format, arg1, arg2); + } + + public void error(Marker marker, String format, Object... arguments) { + error(format, arguments); + } + + public void error(Marker marker, String msg, Throwable t) { + error(msg, t); + } + + public String toString() { + return this.getClass().getName() + "(" + getName() + ")"; + } + +} diff --git a/src/main/java/org/slf4j/helpers/MessageFormatter.java b/src/main/java/org/slf4j/helpers/MessageFormatter.java new file mode 100644 index 0000000..47f95da --- /dev/null +++ b/src/main/java/org/slf4j/helpers/MessageFormatter.java @@ -0,0 +1,430 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +// contributors: lizongbo: proposed special treatment of array parameter values +// Joern Huxhorn: pointed out double[] omission, suggested deep array copy +/** + * Formats messages according to very simple substitution rules. Substitutions + * can be made 1, 2 or more arguments. + * + *

+ * For example, + * + *

+ * MessageFormatter.format("Hi {}.", "there")
+ * 
+ * + * will return the string "Hi there.". + *

+ * The {} pair is called the formatting anchor. It serves to designate + * the location where arguments need to be substituted within the message + * pattern. + *

+ * In case your message contains the '{' or the '}' character, you do not have + * to do anything special unless the '}' character immediately follows '{'. For + * example, + * + *

+ * MessageFormatter.format("Set {1,2,3} is not equal to {}.", "1,2");
+ * 
+ * + * will return the string "Set {1,2,3} is not equal to 1,2.". + * + *

+ * If for whatever reason you need to place the string "{}" in the message + * without its formatting anchor meaning, then you need to escape the + * '{' character with '\', that is the backslash character. Only the '{' + * character should be escaped. There is no need to escape the '}' character. + * For example, + * + *

+ * MessageFormatter.format("Set \\{} is not equal to {}.", "1,2");
+ * 
+ * + * will return the string "Set {} is not equal to 1,2.". + * + *

+ * The escaping behavior just described can be overridden by escaping the escape + * character '\'. Calling + * + *

+ * MessageFormatter.format("File name is C:\\\\{}.", "file.zip");
+ * 
+ * + * will return the string "File name is C:\file.zip". + * + *

+ * The formatting conventions are different from those of {@link MessageFormat} + * which ships with the Java platform. This is justified by the fact that + * SLF4J's implementation is 10 times faster than that of {@link MessageFormat}. + * This local performance difference is both measurable and significant in the + * larger context of the complete logging processing chain. + * + *

+ * See also {@link #format(String, Object)}, + * {@link #format(String, Object, Object)} and + * {@link #arrayFormat(String, Object[])} methods for more details. + * + * @author Ceki Gülcü + * @author Joern Huxhorn + */ +final public class MessageFormatter { + static final char DELIM_START = '{'; + static final char DELIM_STOP = '}'; + static final String DELIM_STR = "{}"; + private static final char ESCAPE_CHAR = '\\'; + + /** + * Performs single argument substitution for the 'messagePattern' passed as + * parameter. + *

+ * For example, + * + *

+     * MessageFormatter.format("Hi {}.", "there");
+     * 
+ * + * will return the string "Hi there.". + *

+ * + * @param messagePattern + * The message pattern which will be parsed and formatted + * @param arg + * The argument to be substituted in place of the formatting anchor + * @return The formatted message + */ + final public static FormattingTuple format(String messagePattern, Object arg) { + return arrayFormat(messagePattern, new Object[] { arg }); + } + + /** + * + * Performs a two argument substitution for the 'messagePattern' passed as + * parameter. + *

+ * For example, + * + *

+     * MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
+     * 
+ * + * will return the string "Hi Alice. My name is Bob.". + * + * @param messagePattern + * The message pattern which will be parsed and formatted + * @param arg1 + * The argument to be substituted in place of the first formatting + * anchor + * @param arg2 + * The argument to be substituted in place of the second formatting + * anchor + * @return The formatted message + */ + final public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) { + return arrayFormat(messagePattern, new Object[] { arg1, arg2 }); + } + + final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) { + Throwable throwableCandidate = MessageFormatter.getThrowableCandidate(argArray); + Object[] args = argArray; + if (throwableCandidate != null) { + args = MessageFormatter.trimmedCopy(argArray); + } + return arrayFormat(messagePattern, args, throwableCandidate); + } + + /** + * Assumes that argArray only contains arguments with no throwable as last element. + * + * @param messagePattern + * @param argArray + */ + final public static String basicArrayFormat(final String messagePattern, final Object[] argArray) { + FormattingTuple ft = arrayFormat(messagePattern, argArray, null); + return ft.getMessage(); + } + + public static String basicArrayFormat(NormalizedParameters np) { + return basicArrayFormat(np.getMessage(), np.getArguments()); + } + + final public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray, Throwable throwable) { + + if (messagePattern == null) { + return new FormattingTuple(null, argArray, throwable); + } + + if (argArray == null) { + return new FormattingTuple(messagePattern); + } + + int i = 0; + int j; + // use string builder for better multicore performance + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + + int L; + for (L = 0; L < argArray.length; L++) { + + j = messagePattern.indexOf(DELIM_STR, i); + + if (j == -1) { + // no more variables + if (i == 0) { // this is a simple string + return new FormattingTuple(messagePattern, argArray, throwable); + } else { // add the tail string which contains no variables and return + // the result. + sbuf.append(messagePattern, i, messagePattern.length()); + return new FormattingTuple(sbuf.toString(), argArray, throwable); + } + } else { + if (isEscapedDelimeter(messagePattern, j)) { + if (!isDoubleEscaped(messagePattern, j)) { + L--; // DELIM_START was escaped, thus should not be incremented + sbuf.append(messagePattern, i, j - 1); + sbuf.append(DELIM_START); + i = j + 1; + } else { + // The escape character preceding the delimiter start is + // itself escaped: "abc x:\\{}" + // we have to consume one backward slash + sbuf.append(messagePattern, i, j - 1); + deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); + i = j + 2; + } + } else { + // normal case + sbuf.append(messagePattern, i, j); + deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); + i = j + 2; + } + } + } + // append the characters following the last {} pair. + sbuf.append(messagePattern, i, messagePattern.length()); + return new FormattingTuple(sbuf.toString(), argArray, throwable); + } + + final static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) { + + if (delimeterStartIndex == 0) { + return false; + } + char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1); + if (potentialEscape == ESCAPE_CHAR) { + return true; + } else { + return false; + } + } + + final static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) { + if (delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) { + return true; + } else { + return false; + } + } + + // special treatment of array values was suggested by 'lizongbo' + private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map seenMap) { + if (o == null) { + sbuf.append("null"); + return; + } + if (!o.getClass().isArray()) { + safeObjectAppend(sbuf, o); + } else { + // check for primitive array types because they + // unfortunately cannot be cast to Object[] + if (o instanceof boolean[]) { + booleanArrayAppend(sbuf, (boolean[]) o); + } else if (o instanceof byte[]) { + byteArrayAppend(sbuf, (byte[]) o); + } else if (o instanceof char[]) { + charArrayAppend(sbuf, (char[]) o); + } else if (o instanceof short[]) { + shortArrayAppend(sbuf, (short[]) o); + } else if (o instanceof int[]) { + intArrayAppend(sbuf, (int[]) o); + } else if (o instanceof long[]) { + longArrayAppend(sbuf, (long[]) o); + } else if (o instanceof float[]) { + floatArrayAppend(sbuf, (float[]) o); + } else if (o instanceof double[]) { + doubleArrayAppend(sbuf, (double[]) o); + } else { + objectArrayAppend(sbuf, (Object[]) o, seenMap); + } + } + } + + private static void safeObjectAppend(StringBuilder sbuf, Object o) { + try { + String oAsString = o.toString(); + sbuf.append(oAsString); + } catch (Throwable t) { + Reporter.error("Failed toString() invocation on an object of type [" + o.getClass().getName() + "]", t); + sbuf.append("[FAILED toString()]"); + } + + } + + private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) { + sbuf.append('['); + if (!seenMap.containsKey(a)) { + seenMap.put(a, null); + final int len = a.length; + for (int i = 0; i < len; i++) { + deeplyAppendParameter(sbuf, a[i], seenMap); + if (i != len - 1) + sbuf.append(", "); + } + // allow repeats in siblings + seenMap.remove(a); + } else { + sbuf.append("..."); + } + sbuf.append(']'); + } + + private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void byteArrayAppend(StringBuilder sbuf, byte[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void charArrayAppend(StringBuilder sbuf, char[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void shortArrayAppend(StringBuilder sbuf, short[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void intArrayAppend(StringBuilder sbuf, int[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void longArrayAppend(StringBuilder sbuf, long[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void floatArrayAppend(StringBuilder sbuf, float[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + private static void doubleArrayAppend(StringBuilder sbuf, double[] a) { + sbuf.append('['); + final int len = a.length; + for (int i = 0; i < len; i++) { + sbuf.append(a[i]); + if (i != len - 1) + sbuf.append(", "); + } + sbuf.append(']'); + } + + /** + * Helper method to determine if an {@link Object} array contains a {@link Throwable} as last element + * + * @param argArray + * The arguments off which we want to know if it contains a {@link Throwable} as last element + * @return if the last {@link Object} in argArray is a {@link Throwable} this method will return it, + * otherwise it returns null + */ + public static Throwable getThrowableCandidate(final Object[] argArray) { + return NormalizedParameters.getThrowableCandidate(argArray); + } + + /** + * Helper method to get all but the last element of an array + * + * @param argArray + * The arguments from which we want to remove the last element + * + * @return a copy of the array without the last element + */ + public static Object[] trimmedCopy(final Object[] argArray) { + return NormalizedParameters.trimmedCopy(argArray); + } + +} diff --git a/src/main/java/org/slf4j/helpers/NOPLogger.java b/src/main/java/org/slf4j/helpers/NOPLogger.java new file mode 100644 index 0000000..4b9a359 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NOPLogger.java @@ -0,0 +1,427 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import org.slf4j.Logger; +import org.slf4j.Marker; + +/** + * A direct NOP (no operation) implementation of {@link Logger}. + * + * @author Ceki Gülcü + */ +public class NOPLogger extends NamedLoggerBase implements Logger { + + private static final long serialVersionUID = -517220405410904473L; + + /** + * The unique instance of NOPLogger. + */ + public static final NOPLogger NOP_LOGGER = new NOPLogger(); + + /** + * There is no point in creating multiple instances of NOPLogger. + * + * The present constructor should be "private" but we are leaving it as "protected" for compatibility. + */ + protected NOPLogger() { + } + + /** + * Always returns the string value "NOP". + */ + @Override + public String getName() { + return "NOP"; + } + + /** + * Always returns false. + * @return always false + */ + @Override + final public boolean isTraceEnabled() { + return false; + } + + /** A NOP implementation. */ + @Override + final public void trace(String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + public final void trace(String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + @Override + public final void trace(String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + final public boolean isDebugEnabled() { + return false; + } + + /** A NOP implementation. */ + final public void debug(String msg) { + // NOP + } + + /** A NOP implementation. */ + final public void debug(String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + final public void debug(String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + final public void debug(String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + final public void debug(String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + final public boolean isInfoEnabled() { + // NOP + return false; + } + + /** A NOP implementation. */ + final public void info(String msg) { + // NOP + } + + /** A NOP implementation. */ + final public void info(String format, Object arg1) { + // NOP + } + + /** A NOP implementation. */ + final public void info(String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + final public void info(String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + final public void info(String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + final public boolean isWarnEnabled() { + return false; + } + + /** A NOP implementation. */ + final public void warn(String msg) { + // NOP + } + + /** A NOP implementation. */ + final public void warn(String format, Object arg1) { + // NOP + } + + /** A NOP implementation. */ + final public void warn(String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + final public void warn(String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + final public void warn(String msg, Throwable t) { + // NOP + } + + /** A NOP implementation. */ + final public boolean isErrorEnabled() { + return false; + } + + /** A NOP implementation. */ + final public void error(String msg) { + // NOP + } + + /** A NOP implementation. */ + final public void error(String format, Object arg1) { + // NOP + } + + /** A NOP implementation. */ + final public void error(String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + final public void error(String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + final public void error(String msg, Throwable t) { + // NOP + } + + // ============================================================ + // Added NOP methods since MarkerIgnoringBase is now deprecated + // ============================================================ + /** + * Always returns false. + * @return always false + */ + final public boolean isTraceEnabled(Marker marker) { + // NOP + return false; + } + + /** A NOP implementation. */ + @Override + final public void trace(Marker marker, String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(Marker marker, String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(Marker marker, String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(Marker marker, String format, Object... argArray) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void trace(Marker marker, String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + final public boolean isDebugEnabled(Marker marker) { + return false; + } + + /** A NOP implementation. */ + @Override + final public void debug(Marker marker, String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void debug(Marker marker, String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void debug(Marker marker, String format, Object arg1, Object arg2) { + // NOP + } + + @Override + final public void debug(Marker marker, String format, Object... arguments) { + // NOP + } + + @Override + final public void debug(Marker marker, String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + @Override + public boolean isInfoEnabled(Marker marker) { + return false; + } + + /** A NOP implementation. */ + @Override + final public void info(Marker marker, String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void info(Marker marker, String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void info(Marker marker, String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void info(Marker marker, String format, Object... arguments) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void info(Marker marker, String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + @Override + final public boolean isWarnEnabled(Marker marker) { + return false; + } + + /** A NOP implementation. */ + @Override + final public void warn(Marker marker, String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void warn(Marker marker, String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void warn(Marker marker, String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void warn(Marker marker, String format, Object... arguments) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void warn(Marker marker, String msg, Throwable t) { + // NOP + } + + /** + * Always returns false. + * @return always false + */ + @Override + final public boolean isErrorEnabled(Marker marker) { + return false; + } + + /** A NOP implementation. */ + @Override + final public void error(Marker marker, String msg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void error(Marker marker, String format, Object arg) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void error(Marker marker, String format, Object arg1, Object arg2) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void error(Marker marker, String format, Object... arguments) { + // NOP + } + + /** A NOP implementation. */ + @Override + final public void error(Marker marker, String msg, Throwable t) { + // NOP + } + // =================================================================== + // End of added NOP methods since MarkerIgnoringBase is now deprecated + // =================================================================== + +} diff --git a/src/main/java/org/slf4j/helpers/NOPLoggerFactory.java b/src/main/java/org/slf4j/helpers/NOPLoggerFactory.java new file mode 100644 index 0000000..7cc8391 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NOPLoggerFactory.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; + +/** + * NOPLoggerFactory is a trivial implementation of {@link + * ILoggerFactory} which always returns the unique instance of + * NOPLogger. + * + * @author Ceki Gülcü + */ +public class NOPLoggerFactory implements ILoggerFactory { + + public NOPLoggerFactory() { + // nothing to do + } + + public Logger getLogger(String name) { + return NOPLogger.NOP_LOGGER; + } + +} diff --git a/src/main/java/org/slf4j/helpers/NOPMDCAdapter.java b/src/main/java/org/slf4j/helpers/NOPMDCAdapter.java new file mode 100644 index 0000000..7c34fca --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NOPMDCAdapter.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.util.Deque; +import java.util.Map; + +import org.slf4j.spi.MDCAdapter; + +/** + * This adapter is an empty implementation of the {@link MDCAdapter} interface. + * It is used for all logging systems which do not support mapped + * diagnostic contexts such as JDK14, simple and NOP. + * + * @author Ceki Gülcü + * + * @since 1.4.1 + */ +public class NOPMDCAdapter implements MDCAdapter { + + public void clear() { + } + + public String get(String key) { + return null; + } + + public void put(String key, String val) { + } + + public void remove(String key) { + } + + public Map getCopyOfContextMap() { + return null; + } + + public void setContextMap(Map contextMap) { + // NOP + } + + @Override + public void pushByKey(String key, String value) { + } + + @Override + public String popByKey(String key) { + return null; + } + + @Override + public Deque getCopyOfDequeByKey(String key) { + return null; + } + + public void clearDequeByKey(String key) { + } + +} diff --git a/src/main/java/org/slf4j/helpers/NOP_FallbackServiceProvider.java b/src/main/java/org/slf4j/helpers/NOP_FallbackServiceProvider.java new file mode 100644 index 0000000..87d287a --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NOP_FallbackServiceProvider.java @@ -0,0 +1,47 @@ +package org.slf4j.helpers; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class NOP_FallbackServiceProvider implements SLF4JServiceProvider { + + /** + * Declare the version of the SLF4J API this implementation is compiled + * against. The value of this field is modified with each major release. + */ + // to avoid constant folding by the compiler, this field must *not* be final + public static String REQUESTED_API_VERSION = "2.0.99"; // !final + + private final ILoggerFactory loggerFactory = new NOPLoggerFactory(); + private final IMarkerFactory markerFactory = new BasicMarkerFactory(); + private final MDCAdapter mdcAdapter = new NOPMDCAdapter(); + + + @Override + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + return REQUESTED_API_VERSION; + } + + @Override + public void initialize() { + // already initialized + } +} diff --git a/src/main/java/org/slf4j/helpers/NamedLoggerBase.java b/src/main/java/org/slf4j/helpers/NamedLoggerBase.java new file mode 100644 index 0000000..97b3183 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NamedLoggerBase.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Serves as base class for named logger implementation. More significantly, this + * class establishes deserialization behavior. + * + * @author Ceki Gulcu + * @see #readResolve + * @since 1.5.3 + */ +abstract class NamedLoggerBase implements Logger, Serializable { + + private static final long serialVersionUID = 7535258609338176893L; + + protected String name; + + public String getName() { + return name; + } + + /** + * Replace this instance with a homonymous (same name) logger returned + * by LoggerFactory. Note that this method is only called during + * deserialization. + * + *

+ * This approach will work well if the desired ILoggerFactory is the one + * referenced by LoggerFactory. However, if the user manages its logger hierarchy + * through a different (non-static) mechanism, e.g. dependency injection, then + * this approach would be mostly counterproductive. + * + * @return logger with same name as returned by LoggerFactory + * @throws ObjectStreamException + */ + protected Object readResolve() throws ObjectStreamException { + // using getName() instead of this.name works even for + // NOPLogger + return LoggerFactory.getLogger(getName()); + } + +} diff --git a/src/main/java/org/slf4j/helpers/NormalizedParameters.java b/src/main/java/org/slf4j/helpers/NormalizedParameters.java new file mode 100644 index 0000000..ec278f4 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/NormalizedParameters.java @@ -0,0 +1,116 @@ +package org.slf4j.helpers; + +import org.slf4j.event.LoggingEvent; + +/** + * Holds normalized call parameters. + * + * Includes utility methods such as {@link #normalize(String, Object[], Throwable)} to help the normalization of parameters. + * + * @author ceki + * @since 2.0 + */ +public class NormalizedParameters { + + final String message; + final Object[] arguments; + final Throwable throwable; + + public NormalizedParameters(String message, Object[] arguments, Throwable throwable) { + this.message = message; + this.arguments = arguments; + this.throwable = throwable; + } + + public NormalizedParameters(String message, Object[] arguments) { + this(message, arguments, null); + } + + public String getMessage() { + return message; + } + + public Object[] getArguments() { + return arguments; + } + + public Throwable getThrowable() { + return throwable; + } + + /** + * Helper method to determine if an {@link Object} array contains a + * {@link Throwable} as last element + * + * @param argArray The arguments off which we want to know if it contains a + * {@link Throwable} as last element + * @return if the last {@link Object} in argArray is a {@link Throwable} this + * method will return it, otherwise it returns null + */ + public static Throwable getThrowableCandidate(final Object[] argArray) { + if (argArray == null || argArray.length == 0) { + return null; + } + + final Object lastEntry = argArray[argArray.length - 1]; + if (lastEntry instanceof Throwable) { + return (Throwable) lastEntry; + } + + return null; + } + + /** + * Helper method to get all but the last element of an array + * + * @param argArray The arguments from which we want to remove the last element + * + * @return a copy of the array without the last element + */ + public static Object[] trimmedCopy(final Object[] argArray) { + if (argArray == null || argArray.length == 0) { + throw new IllegalStateException("non-sensical empty or null argument array"); + } + + final int trimmedLen = argArray.length - 1; + + Object[] trimmed = new Object[trimmedLen]; + + if (trimmedLen > 0) { + System.arraycopy(argArray, 0, trimmed, 0, trimmedLen); + } + + return trimmed; + } + + /** + * This method serves to normalize logging call invocation parameters. + * + * More specifically, if a throwable argument is not supplied directly, it + * attempts to extract it from the argument array. + */ + public static NormalizedParameters normalize(String msg, Object[] arguments, Throwable t) { + + if (t != null) { + return new NormalizedParameters(msg, arguments, t); + } + + if (arguments == null || arguments.length == 0) { + return new NormalizedParameters(msg, arguments, t); + } + + Throwable throwableCandidate = NormalizedParameters.getThrowableCandidate(arguments); + if (throwableCandidate != null) { + Object[] trimmedArguments = MessageFormatter.trimmedCopy(arguments); + return new NormalizedParameters(msg, trimmedArguments, throwableCandidate); + } else { + return new NormalizedParameters(msg, arguments); + } + + } + + public static NormalizedParameters normalize(LoggingEvent event) { + return normalize(event.getMessage(), event.getArgumentArray(), event.getThrowable()); + } + +} diff --git a/src/main/java/org/slf4j/helpers/Reporter.java b/src/main/java/org/slf4j/helpers/Reporter.java new file mode 100644 index 0000000..9091b29 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/Reporter.java @@ -0,0 +1,204 @@ +package org.slf4j.helpers; + +import java.io.PrintStream; + +/** + * An internally used class for reporting internal messages generated by SLF4J itself, typically + * during initialization. + * + *

+ * Internal reporting is performed by calling the {@link #debug(String)}, {@link #info(String)}, + * {@link #warn(String)} (String)} {@link #error(String)} (String)} and + * {@link #error(String, Throwable)} methods. + *

+ *

See {@link #SLF4J_INTERNAL_VERBOSITY_KEY} and {@link #SLF4J_INTERNAL_REPORT_STREAM_KEY} for + * configuration options.

+ *

+ *

+ * Note that this system is independent of the logging back-end in use. + * + * @since 2.0.10 + */ +public class Reporter { + + /** + * this class is used internally by Reporter + */ + private enum Level { + DEBUG(0), INFO(1), WARN(2), ERROR(3); + + int levelInt; + + private Level(int levelInt) { + this.levelInt = levelInt; + } + + private int getLevelInt() { + return levelInt; + } + } + + private enum TargetChoice { + Stderr, Stdout; + } + + static final String SLF4J_DEBUG_PREFIX = "SLF4J(D): "; + static final String SLF4J_INFO_PREFIX = "SLF4J(I): "; + static final String SLF4J_WARN_PREFIX = "SLF4J(W): "; + static final String SLF4J_ERROR_PREFIX = "SLF4J(E): "; + + + /** + * This system property controls the target for internal reports output by SLF4J. + * Recognized values for this key are "System.out", "stdout", "sysout", "System.err", + * "stderr" and "syserr". + * + *

By default, output is directed to "stderr".

+ */ + public static final String SLF4J_INTERNAL_REPORT_STREAM_KEY = "slf4j.internal.report.stream"; + static private final String[] SYSOUT_KEYS = {"System.out", "stdout", "sysout"}; + + /** + * This system property controls the internal level of chattiness + * of SLF4J. Recognized settings are "INFO", "WARN" and "ERROR". The default value is "INFO". + */ + public static final String SLF4J_INTERNAL_VERBOSITY_KEY = "slf4j.internal.verbosity"; + + + static private final TargetChoice TARGET_CHOICE = getTargetChoice(); + + static private final Level INTERNAL_VERBOSITY = initVerbosity(); + + static private TargetChoice getTargetChoice() { + String reportStreamStr = System.getProperty(SLF4J_INTERNAL_REPORT_STREAM_KEY); + + if(reportStreamStr == null || reportStreamStr.isEmpty()) { + return TargetChoice.Stderr; + } + + for(String s : SYSOUT_KEYS) { + if(s.equalsIgnoreCase(reportStreamStr)) + return TargetChoice.Stdout; + } + return TargetChoice.Stderr; + } + + + static private Level initVerbosity() { + String verbosityStr = System.getProperty(SLF4J_INTERNAL_VERBOSITY_KEY); + + if(verbosityStr == null || verbosityStr.isEmpty()) { + return Level.INFO; + } + + if(verbosityStr.equalsIgnoreCase("DEBUG")) { + return Level.DEBUG; + } + + if(verbosityStr.equalsIgnoreCase("ERROR")) { + return Level.ERROR; + } + + + if(verbosityStr.equalsIgnoreCase("WARN")) { + return Level.WARN; + } + + // anything else including verbosityStr.equalsIgnoreCase("INFO") + return Level.INFO; + } + + static boolean isEnabledFor(Level level) { + return (level.levelInt >= INTERNAL_VERBOSITY.levelInt); + } + + static private PrintStream getTarget() { + switch(TARGET_CHOICE) { + case Stdout: + return System.out; + case Stderr: + default: + return System.err; + } + } + + /** + * Report an internal message of level DEBUG. Message text is prefixed with the string "SLF4J(D)", + * with (D) standing as a shorthand for DEBUG. + * + *

Messages of level DEBUG are be enabled when the {@link #SLF4J_INTERNAL_VERBOSITY_KEY} + * system property is set to "DEBUG" and disabled when set to "INFO", "WARN" or "ERROR". By default, + * {@link #SLF4J_INTERNAL_VERBOSITY_KEY} is set to "INFO".

+ * + * @param msg the message text + * @since 2.0.16 + */ + public static void debug(String msg) { + if(isEnabledFor(Level.DEBUG)) { + getTarget().println(SLF4J_DEBUG_PREFIX + msg); + } + } + + /** + * Report an internal message of level INFO. Message text is prefixed with the string "SLF4J(I)", with + * (I) standing as a shorthand for INFO. + * + *

Messages of level INFO are be enabled when the {@link #SLF4J_INTERNAL_VERBOSITY_KEY} system property is + * set to "DEBUG" or "INFO" and disabled when set to "WARN" or "ERROR". By default, + * {@link #SLF4J_INTERNAL_VERBOSITY_KEY} is set to "INFO".

+ * + * @param msg the message text + */ + public static void info(String msg) { + if(isEnabledFor(Level.INFO)) { + getTarget().println(SLF4J_INFO_PREFIX + msg); + } + } + + + /** + * Report an internal message of level "WARN". Message text is prefixed with the string "SLF4J(W)", with + * (W) standing as a shorthand for WARN. + * + *

Messages of level WARN are be enabled when the {@link #SLF4J_INTERNAL_VERBOSITY_KEY} system property is + * set to "DEBUG", "INFO" or "WARN" and disabled when set to "ERROR". By default, + * {@link #SLF4J_INTERNAL_VERBOSITY_KEY} is set to "INFO".

+ * + * @param msg the message text + */ + static final public void warn(String msg) { + if(isEnabledFor(Level.WARN)) { + getTarget().println(SLF4J_WARN_PREFIX + msg); + } + } + + /** + * Report an internal message of level "ERROR accompanied by a {@link Throwable}. + * Message text is prefixed with the string "SLF4J(E)", with (E) standing as a shorthand for ERROR. + * + *

Messages of level ERROR are always enabled. + * + * @param msg the message text + * @param t a Throwable + */ + static final public void error(String msg, Throwable t) { + // error cannot be disabled + getTarget().println(SLF4J_ERROR_PREFIX + msg); + getTarget().println(SLF4J_ERROR_PREFIX + "Reported exception:"); + t.printStackTrace(getTarget()); + } + + /** + * Report an internal message of level "ERROR". Message text is prefixed with the string "SLF4J(E)", + * with (E) standing as a shorthand for ERROR. + * + *

Messages of level ERROR are always enabled. + * + * @param msg the message text + */ + + static final public void error(String msg) { + // error cannot be disabled + getTarget().println(SLF4J_ERROR_PREFIX + msg); + } +} diff --git a/src/main/java/org/slf4j/helpers/Slf4jEnvUtil.java b/src/main/java/org/slf4j/helpers/Slf4jEnvUtil.java new file mode 100644 index 0000000..afe01b4 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/Slf4jEnvUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2024 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.helpers; + +/** + * Various utility methods + * + * @since 2.0.14 + */ +public class Slf4jEnvUtil { + + + /** + * Returns the current version of slf4j, or null if data is not available. + * + * @return current version or null if missing version data + * @since 2.0.14 + */ + static public String slf4jVersion() { +// String moduleVersion = slf4jVersionByModule(); +// if(moduleVersion != null) +// return moduleVersion; + + Package pkg = Slf4jEnvUtil.class.getPackage(); + if(pkg == null) { + return null; + } + final String pkgVersion = pkg.getImplementationVersion(); + return pkgVersion; + } + + /** + * Returns the current version of slf4j via class.getModule() + * or null if data is not available. + * + * @return current version or null if missing version data + * @since 2.0.14 + */ +// static private String slf4jVersionByModule() { +// Module module = Slf4jEnvUtil.class.getModule(); +// if (module == null) +// return null; +// +// ModuleDescriptor md = module.getDescriptor(); +// if (md == null) +// return null; +// Optional opt = md.rawVersion(); +// return opt.orElse(null); +// } + +} diff --git a/src/main/java/org/slf4j/helpers/SubstituteLogger.java b/src/main/java/org/slf4j/helpers/SubstituteLogger.java new file mode 100644 index 0000000..b9e160d --- /dev/null +++ b/src/main/java/org/slf4j/helpers/SubstituteLogger.java @@ -0,0 +1,493 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Queue; + +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.EventRecordingLogger; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; +import org.slf4j.event.SubstituteLoggingEvent; +import org.slf4j.spi.LoggingEventBuilder; + +/** + * A logger implementation which logs via a delegate logger. By default, the delegate is a + * {@link NOPLogger}. However, a different delegate can be set at any time. + * + *

See also the relevant + * error code documentation. + * + * @author Chetan Mehrotra + * @author Ceki Gulcu + */ +public class SubstituteLogger implements Logger { + + private final String name; + private volatile Logger _delegate; + private Boolean delegateEventAware; + private Method logMethodCache; + private EventRecordingLogger eventRecordingLogger; + private final Queue eventQueue; + + public final boolean createdPostInitialization; + + public SubstituteLogger(String name, Queue eventQueue, boolean createdPostInitialization) { + this.name = name; + this.eventQueue = eventQueue; + this.createdPostInitialization = createdPostInitialization; + } + + @Override + public String getName() { + return name; + } + + @Override + public LoggingEventBuilder makeLoggingEventBuilder(Level level) { + return delegate().makeLoggingEventBuilder(level); + } + + @Override + public LoggingEventBuilder atLevel(Level level) { + return delegate().atLevel(level); + } + + @Override + public boolean isEnabledForLevel(Level level) { + return delegate().isEnabledForLevel(level); + } + + @Override + public boolean isTraceEnabled() { + return delegate().isTraceEnabled(); + } + + @Override + public void trace(String msg) { + delegate().trace(msg); + } + + @Override + public void trace(String format, Object arg) { + delegate().trace(format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + delegate().trace(format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + delegate().trace(format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + delegate().trace(msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return delegate().isTraceEnabled(marker); + } + + @Override + public void trace(Marker marker, String msg) { + delegate().trace(marker, msg); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + delegate().trace(marker, format, arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + delegate().trace(marker, format, arg1, arg2); + } + @Override + public void trace(Marker marker, String format, Object... arguments) { + delegate().trace(marker, format, arguments); + } + @Override + public void trace(Marker marker, String msg, Throwable t) { + delegate().trace(marker, msg, t); + } + + @Override + public LoggingEventBuilder atTrace() { + return delegate().atTrace(); + } + + @Override + public boolean isDebugEnabled() { + return delegate().isDebugEnabled(); + } + + @Override + public void debug(String msg) { + delegate().debug(msg); + } + + @Override + public void debug(String format, Object arg) { + delegate().debug(format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + delegate().debug(format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + delegate().debug(format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + delegate().debug(msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return delegate().isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String msg) { + delegate().debug(marker, msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + delegate().debug(marker, format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + delegate().debug(marker, format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + delegate().debug(marker, format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + delegate().debug(marker, msg, t); + } + + @Override + public LoggingEventBuilder atDebug() { + return delegate().atDebug(); + } + + @Override + public boolean isInfoEnabled() { + return delegate().isInfoEnabled(); + } + + + @Override + public void info(String msg) { + delegate().info(msg); + } + + @Override + public void info(String format, Object arg) { + delegate().info(format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + delegate().info(format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + delegate().info(format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + delegate().info(msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return delegate().isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String msg) { + delegate().info(marker, msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + delegate().info(marker, format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + delegate().info(marker, format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + delegate().info(marker, format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + delegate().info(marker, msg, t); + } + + @Override + public LoggingEventBuilder atInfo() { + return delegate().atInfo(); + } + + + @Override + public boolean isWarnEnabled() { + return delegate().isWarnEnabled(); + } + + @Override + public void warn(String msg) { + delegate().warn(msg); + } + + @Override + public void warn(String format, Object arg) { + delegate().warn(format, arg); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + delegate().warn(format, arg1, arg2); + } + + @Override + public void warn(String format, Object... arguments) { + delegate().warn(format, arguments); + } + + @Override + public void warn(String msg, Throwable t) { + delegate().warn(msg, t); + } + + public boolean isWarnEnabled(Marker marker) { + return delegate().isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String msg) { + delegate().warn(marker, msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + delegate().warn(marker, format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + delegate().warn(marker, format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + delegate().warn(marker, format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + delegate().warn(marker, msg, t); + } + + @Override + public LoggingEventBuilder atWarn() { + return delegate().atWarn(); + } + + + + @Override + public boolean isErrorEnabled() { + return delegate().isErrorEnabled(); + } + + @Override + public void error(String msg) { + delegate().error(msg); + } + + @Override + public void error(String format, Object arg) { + delegate().error(format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + delegate().error(format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + delegate().error(format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + delegate().error(msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return delegate().isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String msg) { + delegate().error(marker, msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + delegate().error(marker, format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + delegate().error(marker, format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + delegate().error(marker, format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + delegate().error(marker, msg, t); + } + + @Override + public LoggingEventBuilder atError() { + return delegate().atError(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SubstituteLogger that = (SubstituteLogger) o; + + if (!name.equals(that.name)) + return false; + + return true; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * Return the delegate logger instance if set. Otherwise, return a {@link NOPLogger} + * instance. + */ + public Logger delegate() { + if (_delegate != null) { + return _delegate; + } + if (createdPostInitialization) { + return NOPLogger.NOP_LOGGER; + } else { + return getEventRecordingLogger(); + } + } + + private Logger getEventRecordingLogger() { + if (eventRecordingLogger == null) { + eventRecordingLogger = new EventRecordingLogger(this, eventQueue); + } + return eventRecordingLogger; + } + + /** + * Typically called after the {@link org.slf4j.LoggerFactory} initialization phase is completed. + * @param delegate + */ + public void setDelegate(Logger delegate) { + this._delegate = delegate; + } + + public boolean isDelegateEventAware() { + if (delegateEventAware != null) + return delegateEventAware; + + try { + logMethodCache = _delegate.getClass().getMethod("log", LoggingEvent.class); + delegateEventAware = Boolean.TRUE; + } catch (NoSuchMethodException e) { + delegateEventAware = Boolean.FALSE; + } + return delegateEventAware; + } + + public void log(LoggingEvent event) { + if (isDelegateEventAware()) { + try { + logMethodCache.invoke(_delegate, event); + } catch (IllegalAccessException e) { + } catch (IllegalArgumentException e) { + } catch (InvocationTargetException e) { + } + } + } + + public boolean isDelegateNull() { + return _delegate == null; + } + + public boolean isDelegateNOP() { + return _delegate instanceof NOPLogger; + } +} diff --git a/src/main/java/org/slf4j/helpers/SubstituteLoggerFactory.java b/src/main/java/org/slf4j/helpers/SubstituteLoggerFactory.java new file mode 100644 index 0000000..8a9d5dd --- /dev/null +++ b/src/main/java/org/slf4j/helpers/SubstituteLoggerFactory.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.event.SubstituteLoggingEvent; + +/** + * SubstituteLoggerFactory manages instances of {@link SubstituteLogger}. + * + * @author Ceki Gülcü + * @author Chetan Mehrotra + */ +public class SubstituteLoggerFactory implements ILoggerFactory { + + volatile boolean postInitialization = false; + + final Map loggers = new ConcurrentHashMap<>(); + + final LinkedBlockingQueue eventQueue = new LinkedBlockingQueue<>(); + + synchronized public Logger getLogger(String name) { + SubstituteLogger logger = loggers.get(name); + if (logger == null) { + logger = new SubstituteLogger(name, eventQueue, postInitialization); + loggers.put(name, logger); + } + return logger; + } + + public List getLoggerNames() { + return new ArrayList<>(loggers.keySet()); + } + + public List getLoggers() { + return new ArrayList<>(loggers.values()); + } + + public LinkedBlockingQueue getEventQueue() { + return eventQueue; + } + + public void postInitialization() { + postInitialization = true; + } + + public void clear() { + loggers.clear(); + eventQueue.clear(); + } +} diff --git a/src/main/java/org/slf4j/helpers/SubstituteServiceProvider.java b/src/main/java/org/slf4j/helpers/SubstituteServiceProvider.java new file mode 100644 index 0000000..ad03eae --- /dev/null +++ b/src/main/java/org/slf4j/helpers/SubstituteServiceProvider.java @@ -0,0 +1,50 @@ +package org.slf4j.helpers; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class SubstituteServiceProvider implements SLF4JServiceProvider { + private final SubstituteLoggerFactory loggerFactory = new SubstituteLoggerFactory(); + + // LoggerFactory expects providers to initialize markerFactory as early as possible. + private final IMarkerFactory markerFactory; + + // LoggerFactory expects providers to initialize their MDCAdapter field + // as early as possible, preferably at construction time. + private final MDCAdapter mdcAdapter; + + public SubstituteServiceProvider() { + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new BasicMDCAdapter(); + } + + @Override + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + public SubstituteLoggerFactory getSubstituteLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + throw new UnsupportedOperationException(); + } + + @Override + public void initialize() { + } +} diff --git a/src/main/java/org/slf4j/helpers/ThreadLocalMapOfStacks.java b/src/main/java/org/slf4j/helpers/ThreadLocalMapOfStacks.java new file mode 100644 index 0000000..89ddee0 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/ThreadLocalMapOfStacks.java @@ -0,0 +1,89 @@ +package org.slf4j.helpers; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; + + +/** + * A simple implementation of ThreadLocal backed Map containing values of type + * Deque. + * + * @author Ceki Guuml;cü + * @since 2.0.0 + */ +public class ThreadLocalMapOfStacks { + + // BEWARE: Keys or values placed in a ThreadLocal should not be of a type/class + // not included in the JDK. See also https://jira.qos.ch/browse/LOGBACK-450 + + final ThreadLocal>> tlMapOfStacks = new ThreadLocal<>(); + + public void pushByKey(String key, String value) { + if (key == null) + return; + + Map> map = tlMapOfStacks.get(); + + if (map == null) { + map = new HashMap<>(); + tlMapOfStacks.set(map); + } + + Deque deque = map.get(key); + if (deque == null) { + deque = new ArrayDeque<>(); + } + deque.push(value); + map.put(key, deque); + } + + public String popByKey(String key) { + if (key == null) + return null; + + Map> map = tlMapOfStacks.get(); + if (map == null) + return null; + Deque deque = map.get(key); + if (deque == null) + return null; + return deque.pop(); + } + + public Deque getCopyOfDequeByKey(String key) { + if (key == null) + return null; + + Map> map = tlMapOfStacks.get(); + if (map == null) + return null; + Deque deque = map.get(key); + if (deque == null) + return null; + + return new ArrayDeque(deque); + } + + /** + * Clear the deque(stack) referenced by 'key'. + * + * @param key identifies the stack + * + * @since 2.0.0 + */ + public void clearDequeByKey(String key) { + if (key == null) + return; + + Map> map = tlMapOfStacks.get(); + if (map == null) + return; + Deque deque = map.get(key); + if (deque == null) + return; + deque.clear(); + } + +} diff --git a/src/main/java/org/slf4j/helpers/Util.java b/src/main/java/org/slf4j/helpers/Util.java new file mode 100644 index 0000000..c56066c --- /dev/null +++ b/src/main/java/org/slf4j/helpers/Util.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.helpers; + +/** + * An internal utility class. + * + * @author Alexander Dorokhine + * @author Ceki Gülcü + */ +public final class Util { + + private Util() { + } + + public static String safeGetSystemProperty(String key) { + if (key == null) + throw new IllegalArgumentException("null input"); + + String result = null; + try { + result = System.getProperty(key); + } catch (SecurityException sm) { + ; // ignore + } + return result; + } + + public static boolean safeGetBooleanSystemProperty(String key) { + String value = safeGetSystemProperty(key); + if (value == null) + return false; + else + return value.equalsIgnoreCase("true"); + } + + /** + * In order to call {@link SecurityManager#getClassContext()}, which is a + * protected method, we add this wrapper which allows the method to be visible + * inside this package. + */ + private static final class ClassContextSecurityManager extends SecurityManager { + protected Class[] getClassContext() { + return super.getClassContext(); + } + } + + private static ClassContextSecurityManager SECURITY_MANAGER; + private static boolean SECURITY_MANAGER_CREATION_ALREADY_ATTEMPTED = false; + + private static ClassContextSecurityManager getSecurityManager() { + if (SECURITY_MANAGER != null) + return SECURITY_MANAGER; + else if (SECURITY_MANAGER_CREATION_ALREADY_ATTEMPTED) + return null; + else { + SECURITY_MANAGER = safeCreateSecurityManager(); + SECURITY_MANAGER_CREATION_ALREADY_ATTEMPTED = true; + return SECURITY_MANAGER; + } + } + + private static ClassContextSecurityManager safeCreateSecurityManager() { + try { + return new ClassContextSecurityManager(); + } catch (SecurityException sm) { + return null; + } + } + + /** + * Returns the name of the class which called the invoking method. + * + * @return the name of the class which called the invoking method. + */ + public static Class getCallingClass() { + ClassContextSecurityManager securityManager = getSecurityManager(); + if (securityManager == null) + return null; + Class[] trace = securityManager.getClassContext(); + String thisClassName = Util.class.getName(); + + // Advance until Util is found + int i; + for (i = 0; i < trace.length; i++) { + if (thisClassName.equals(trace[i].getName())) + break; + } + + // trace[i] = Util; trace[i+1] = caller; trace[i+2] = caller's caller + if (i >= trace.length || i + 2 >= trace.length) { + throw new IllegalStateException("Failed to find org.slf4j.helpers.Util or its caller in the stack; " + "this should not happen"); + } + + return trace[i + 2]; + } + + /** + * See {@link Reporter#error(String, Throwable)} class for alternative. + * + * @deprecated replaced by the {@link Reporter#error(String, Throwable)} method. + * @param msg message to print + * @param t throwable to print + */ + static final public void report(String msg, Throwable t) { + System.err.println(msg); + System.err.println("Reported exception:"); + t.printStackTrace(); + } + + /** + * See {@link Reporter} class for alternatives. + * + * @deprecated replaced by one of {@link Reporter#info(String)}, + * {@link Reporter#warn(String)} or {@link Reporter#error(String)} methods. + * @param msg message to print + */ + static final public void report(String msg) { + System.err.println("SLF4J: " + msg); + } + +} diff --git a/src/main/java/org/slf4j/helpers/package.html b/src/main/java/org/slf4j/helpers/package.html new file mode 100644 index 0000000..f223543 --- /dev/null +++ b/src/main/java/org/slf4j/helpers/package.html @@ -0,0 +1,16 @@ + + + + + + + + + + + +

Helper classes.

+ +
+ + diff --git a/src/main/java/org/slf4j/package.html b/src/main/java/org/slf4j/package.html new file mode 100644 index 0000000..323bac2 --- /dev/null +++ b/src/main/java/org/slf4j/package.html @@ -0,0 +1,16 @@ + + + + + + + + + + + +

Core logging interfaces.

+ +
+ + diff --git a/src/main/java/org/slf4j/spi/CallerBoundaryAware.java b/src/main/java/org/slf4j/spi/CallerBoundaryAware.java new file mode 100644 index 0000000..50777ec --- /dev/null +++ b/src/main/java/org/slf4j/spi/CallerBoundaryAware.java @@ -0,0 +1,25 @@ +package org.slf4j.spi; + +import org.slf4j.event.LoggingEvent; + +/** + * Additional interface to {@link LoggingEventBuilder} and + * {@link LoggingEvent LoggingEvent}. + * + * Implementations of {@link LoggingEventBuilder} and {@link LoggingEvent} may optionally + * implement {@link CallerBoundaryAware} in order to support caller info extraction. + * + * This interface is intended for use by logging backends or logging bridges. + * + * @author Ceki Gulcu + * + */ +public interface CallerBoundaryAware { + + /** + * Add a fqcn (fully qualified class name) to this event, presumed to be the caller boundary. + * + * @param fqcn + */ + void setCallerBoundary(String fqcn); +} diff --git a/src/main/java/org/slf4j/spi/DefaultLoggingEventBuilder.java b/src/main/java/org/slf4j/spi/DefaultLoggingEventBuilder.java new file mode 100644 index 0000000..5752ad4 --- /dev/null +++ b/src/main/java/org/slf4j/spi/DefaultLoggingEventBuilder.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2004-2022 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.slf4j.spi; + +import java.util.List; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.DefaultLoggingEvent; +import org.slf4j.event.KeyValuePair; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +/** + * Default implementation of {@link LoggingEventBuilder} + */ +public class DefaultLoggingEventBuilder implements LoggingEventBuilder, CallerBoundaryAware { + + + // The caller boundary when the log() methods are invoked, is this class itself. + + static String DLEB_FQCN = DefaultLoggingEventBuilder.class.getName(); + + protected DefaultLoggingEvent loggingEvent; + protected Logger logger; + + public DefaultLoggingEventBuilder(Logger logger, Level level) { + this.logger = logger; + loggingEvent = new DefaultLoggingEvent(level, logger); + } + + /** + * Add a marker to the current logging event being built. + *

+ * It is possible to add multiple markers to the same logging event. + * + * @param marker the marker to add + */ + @Override + public LoggingEventBuilder addMarker(Marker marker) { + loggingEvent.addMarker(marker); + return this; + } + + @Override + public LoggingEventBuilder setCause(Throwable t) { + loggingEvent.setThrowable(t); + return this; + } + + @Override + public LoggingEventBuilder addArgument(Object p) { + this.loggingEvent.addArgument(p); + return this; + } + + @Override + public LoggingEventBuilder addArgument(Supplier objectSupplier) { + this.loggingEvent.addArgument(objectSupplier.get()); + return this; + } + + @Override + public LoggingEventBuilder addKeyValue(String key, Object value) { + loggingEvent.addKeyValue(key, value); + return this; + } + + @Override + public LoggingEventBuilder addKeyValue(String key, Supplier value) { + loggingEvent.addKeyValue(key, value.get()); + return this; + } + + @Override + public void setCallerBoundary(String fqcn) { + this.loggingEvent.setCallerBoundary(fqcn); + } + + @Override + public void log() { + log(this.loggingEvent); + } + + @Override + public LoggingEventBuilder setMessage(String message) { + this.loggingEvent.setMessage(message); + return this; + } + + @Override + public LoggingEventBuilder setMessage(Supplier messageSupplier) { + this.loggingEvent.setMessage(messageSupplier.get()); + return this; + } + + @Override + public void log(String message) { + this.loggingEvent.setMessage(message); + log(this.loggingEvent); + } + + @Override + public void log(String message, Object arg) { + this.loggingEvent.setMessage(message); + this.loggingEvent.addArgument(arg); + log(this.loggingEvent); + } + + @Override + public void log(String message, Object arg0, Object arg1) { + this.loggingEvent.setMessage(message); + this.loggingEvent.addArgument(arg0); + this.loggingEvent.addArgument(arg1); + log(this.loggingEvent); + } + + @Override + public void log(String message, Object... args) { + this.loggingEvent.setMessage(message); + this.loggingEvent.addArguments(args); + + log(this.loggingEvent); + } + + @Override + public void log(Supplier messageSupplier) { + if(messageSupplier == null) { + log((String) null); + } else { + log(messageSupplier.get()); + } + } + + protected void log(LoggingEvent aLoggingEvent) { + if(aLoggingEvent.getCallerBoundary() == null) { + setCallerBoundary(DLEB_FQCN); + } + + if(logger instanceof LoggingEventAware) { + ((LoggingEventAware) logger).log(aLoggingEvent); + } else if(logger instanceof LocationAwareLogger) { + logViaLocationAwareLoggerAPI((LocationAwareLogger) logger, aLoggingEvent); + } else { + logViaPublicSLF4JLoggerAPI(aLoggingEvent); + } + } + + private void logViaLocationAwareLoggerAPI(LocationAwareLogger locationAwareLogger, LoggingEvent aLoggingEvent) { + String msg = aLoggingEvent.getMessage(); + List markerList = aLoggingEvent.getMarkers(); + String mergedMessage = mergeMarkersAndKeyValuePairsAndMessage(aLoggingEvent); + locationAwareLogger.log(null, aLoggingEvent.getCallerBoundary(), aLoggingEvent.getLevel().toInt(), + mergedMessage, + aLoggingEvent.getArgumentArray(), aLoggingEvent.getThrowable()); + } + + private void logViaPublicSLF4JLoggerAPI(LoggingEvent aLoggingEvent) { + Object[] argArray = aLoggingEvent.getArgumentArray(); + int argLen = argArray == null ? 0 : argArray.length; + + Throwable t = aLoggingEvent.getThrowable(); + int tLen = t == null ? 0 : 1; + + Object[] combinedArguments = new Object[argLen + tLen]; + + if(argArray != null) { + System.arraycopy(argArray, 0, combinedArguments, 0, argLen); + } + if(t != null) { + combinedArguments[argLen] = t; + } + + String mergedMessage = mergeMarkersAndKeyValuePairsAndMessage(aLoggingEvent); + + + switch(aLoggingEvent.getLevel()) { + case TRACE: + logger.trace(mergedMessage, combinedArguments); + break; + case DEBUG: + logger.debug(mergedMessage, combinedArguments); + break; + case INFO: + logger.info(mergedMessage, combinedArguments); + break; + case WARN: + logger.warn(mergedMessage, combinedArguments); + break; + case ERROR: + logger.error(mergedMessage, combinedArguments); + break; + } + } + + + /** + * Prepend markers and key-value pairs to the message. + * + * @param aLoggingEvent + * + * @return + */ + private String mergeMarkersAndKeyValuePairsAndMessage(LoggingEvent aLoggingEvent) { + StringBuilder sb = mergeMarkers(aLoggingEvent.getMarkers(), null); + sb = mergeKeyValuePairs(aLoggingEvent.getKeyValuePairs(), sb); + final String mergedMessage = mergeMessage(aLoggingEvent.getMessage(), sb); + return mergedMessage; + } + + private StringBuilder mergeMarkers(List markerList, StringBuilder sb) { + if(markerList == null || markerList.isEmpty()) + return sb; + + if(sb == null) + sb = new StringBuilder(); + + for(Marker marker : markerList) { + sb.append(marker); + sb.append(' '); + } + return sb; + } + + private StringBuilder mergeKeyValuePairs(List keyValuePairList, StringBuilder sb) { + if(keyValuePairList == null || keyValuePairList.isEmpty()) + return sb; + + if(sb == null) + sb = new StringBuilder(); + + for(KeyValuePair kvp : keyValuePairList) { + sb.append(kvp.key); + sb.append('='); + sb.append(kvp.value); + sb.append(' '); + } + return sb; + } + + private String mergeMessage(String msg, StringBuilder sb) { + if(sb != null) { + sb.append(msg); + return sb.toString(); + } else { + return msg; + } + } + + + + + + +} diff --git a/src/main/java/org/slf4j/spi/LocationAwareLogger.java b/src/main/java/org/slf4j/spi/LocationAwareLogger.java new file mode 100644 index 0000000..d0d1a31 --- /dev/null +++ b/src/main/java/org/slf4j/spi/LocationAwareLogger.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.spi; + +import org.slf4j.Logger; +import org.slf4j.Marker; + +/** + * An optional interface helping integration with logging systems capable of + * extracting location information. This interface is mainly used by SLF4J bridges + * such as jcl-over-slf4j, jul-to-slf4j and log4j-over-slf4j or {@link Logger} wrappers + * which need to provide hints so that the underlying logging system can extract + * the correct location information (method name, line number). + * + * @author Ceki Gülcü + * @since 1.3 + */ +public interface LocationAwareLogger extends Logger { + + // these constants should be in EventConstants. However, in order to preserve binary backward compatibility + // we keep these constants here. {@link EventConstants} redefines these constants using the values below. + final public int TRACE_INT = 00; + final public int DEBUG_INT = 10; + final public int INFO_INT = 20; + final public int WARN_INT = 30; + final public int ERROR_INT = 40; + + /** + * Printing method with support for location information. + * + * @param marker The marker to be used for this event, may be null. + * + * @param fqcn The fully qualified class name of the logger instance, + * typically the logger class, logger bridge or a logger wrapper. + * + * @param level One of the level integers defined in this interface + * + * @param message The message for the log event + * @param t Throwable associated with the log event, may be null. + */ + public void log(Marker marker, String fqcn, int level, String message, Object[] argArray, Throwable t); + +} diff --git a/src/main/java/org/slf4j/spi/LoggerFactoryBinder.java b/src/main/java/org/slf4j/spi/LoggerFactoryBinder.java new file mode 100644 index 0000000..169691f --- /dev/null +++ b/src/main/java/org/slf4j/spi/LoggerFactoryBinder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.spi; + +import org.slf4j.ILoggerFactory; + +/** + * An internal interface which helps the static {@link org.slf4j.LoggerFactory} + * class bind with the appropriate {@link ILoggerFactory} instance. + * + * @author Ceki Gülcü + * @deprecated + */ +public interface LoggerFactoryBinder { + + /** + * Return the instance of {@link ILoggerFactory} that + * {@link org.slf4j.LoggerFactory} class should bind to. + * + * @return the instance of {@link ILoggerFactory} that + * {@link org.slf4j.LoggerFactory} class should bind to. + */ + public ILoggerFactory getLoggerFactory(); + + /** + * The String form of the {@link ILoggerFactory} object that this + * LoggerFactoryBinder instance is intended to return. + * + *

This method allows the developer to interrogate this binder's intention + * which may be different from the {@link ILoggerFactory} instance it is able to + * yield in practice. The discrepancy should only occur in case of errors. + * + * @return the class name of the intended {@link ILoggerFactory} instance + */ + public String getLoggerFactoryClassStr(); +} diff --git a/src/main/java/org/slf4j/spi/LoggingEventAware.java b/src/main/java/org/slf4j/spi/LoggingEventAware.java new file mode 100644 index 0000000..b833e40 --- /dev/null +++ b/src/main/java/org/slf4j/spi/LoggingEventAware.java @@ -0,0 +1,23 @@ +package org.slf4j.spi; + +import org.slf4j.event.LoggingEvent; + +/** + * A logger capable of logging from org.slf4j.event.LoggingEvent implements this interface. + * + *

Please note that when the {@link #log(LoggingEvent)} method assumes that + * the event was filtered beforehand and no further filtering needs to occur by the method itself. + *

+ * + *

Implementations of this interface may apply further filtering but they are not + * required to do so. In other words, {@link #log(LoggingEvent)} method is free to assume that + * the event was filtered beforehand and no further filtering needs to occur in the method itself.

+ * + * See also https://jira.qos.ch/browse/SLF4J-575 + * + * @author Ceki Gulcu + * @since 2.0.0 + */ +public interface LoggingEventAware { + void log(LoggingEvent event); +} diff --git a/src/main/java/org/slf4j/spi/LoggingEventBuilder.java b/src/main/java/org/slf4j/spi/LoggingEventBuilder.java new file mode 100644 index 0000000..55e24cf --- /dev/null +++ b/src/main/java/org/slf4j/spi/LoggingEventBuilder.java @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2004-2021 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.spi; + +import java.util.function.Supplier; + +import org.slf4j.Marker; + +import org.slf4j.helpers.CheckReturnValue; + +/** + * This is the main interface in slf4j's fluent API for creating + * {@link org.slf4j.event.LoggingEvent logging events}. + * + * @author Ceki Gülcü + * @since 2.0.0 + */ +public interface LoggingEventBuilder { + + /** + * Set the cause for the logging event being built. + * @param cause a throwable + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder setCause(Throwable cause); + + /** + * A {@link Marker marker} to the event being built. + * + * @param marker a Marker instance to add. + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder addMarker(Marker marker); + + /** + * Add an argument to the event being built. + * + * @param p an Object to add. + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder addArgument(Object p); + + /** + * Add an argument supplier to the event being built. + * + * @param objectSupplier an Object supplier to add. + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder addArgument(Supplier objectSupplier); + + + /** + * Add a {@link org.slf4j.event.KeyValuePair key value pair} to the event being built. + * + * @param key the key of the key value pair. + * @param value the value of the key value pair. + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder addKeyValue(String key, Object value); + + /** + * Add a {@link org.slf4j.event.KeyValuePair key value pair} to the event being built. + * + * @param key the key of the key value pair. + * @param valueSupplier a supplier of a value for the key value pair. + * @return a LoggingEventBuilder, usually this. + */ + @CheckReturnValue + LoggingEventBuilder addKeyValue(String key, Supplier valueSupplier); + + /** + * Sets the message of the logging event. + * + * @since 2.0.0-beta0 + */ + @CheckReturnValue + LoggingEventBuilder setMessage(String message); + + /** + * Sets the message of the event via a message supplier. + * + * @param messageSupplier supplies a String to be used as the message for the event + * @since 2.0.0-beta0 + */ + @CheckReturnValue + LoggingEventBuilder setMessage(Supplier messageSupplier); + + /** + * After the logging event is built, performs actual logging. This method must be called + * for logging to occur. + * + * If the call to {@link #log()} is omitted, a {@link org.slf4j.event.LoggingEvent LoggingEvent} + * will be built but no logging will occur. + * + * @since 2.0.0-beta0 + */ + void log(); + + /** + * Equivalent to calling {@link #setMessage(String)} followed by {@link #log()}; + * + * @param message the message to log + */ + void log(String message); + + /** + * Equivalent to calling {@link #setMessage(String)} followed by {@link #addArgument(Object)}} + * and then {@link #log()} + * + * @param message the message to log + * @param arg an argument to be used with the message to log + */ + void log(String message, Object arg); + + /** + * Equivalent to calling {@link #setMessage(String)} followed by two calls to + * {@link #addArgument(Object)} and then {@link #log()} + * + * @param message the message to log + * @param arg0 first argument to be used with the message to log + * @param arg1 second argument to be used with the message to log + */ + void log(String message, Object arg0, Object arg1); + + + /** + * Equivalent to calling {@link #setMessage(String)} followed by zero or more calls to + * {@link #addArgument(Object)} (depending on the size of args array) and then {@link #log()} + * + * @param message the message to log + * @param args a list (actually an array) of arguments to be used with the message to log + */ + void log(String message, Object... args); + + /** + * Equivalent to calling {@link #setMessage(Supplier)} followed by {@link #log()} + * + * @param messageSupplier a Supplier returning a message of type String + */ + void log(Supplier messageSupplier); + +} diff --git a/src/main/java/org/slf4j/spi/MDCAdapter.java b/src/main/java/org/slf4j/spi/MDCAdapter.java new file mode 100644 index 0000000..924849d --- /dev/null +++ b/src/main/java/org/slf4j/spi/MDCAdapter.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.spi; + +import java.util.Deque; +import java.util.Map; + +/** + * This interface abstracts the service offered by various MDC + * implementations. + * + * @author Ceki Gülcü + * @since 1.4.1 + */ +public interface MDCAdapter { + + /** + * Put a context value (the val parameter) as identified with + * the key parameter into the current thread's context map. + * The key parameter cannot be null. The val parameter + * can be null only if the underlying implementation supports it. + * + *

If the current thread does not have a context map it is created as a side + * effect of this call. + */ + public void put(String key, String val); + + /** + * Get the context identified by the key parameter. + * The key parameter cannot be null. + * + * @return the string value identified by the key parameter. + */ + public String get(String key); + + /** + * Remove the context identified by the key parameter. + * The key parameter cannot be null. + * + *

+ * This method does nothing if there is no previous value + * associated with key. + */ + public void remove(String key); + + /** + * Clear all entries in the MDC. + */ + public void clear(); + + /** + * Return a copy of the current thread's context map, with keys and + * values of type String. Returned value may be null. + * + * @return A copy of the current thread's context map. May be null. + * @since 1.5.1 + */ + public Map getCopyOfContextMap(); + + /** + * Set the current thread's context map by first clearing any existing + * map and then copying the map passed as parameter. The context map + * parameter must only contain keys and values of type String. + * + * Implementations must support null valued map passed as parameter. + * + * @param contextMap must contain only keys and values of type String + * + * @since 1.5.1 + */ + public void setContextMap(Map contextMap); + + /** + * Push a value into the deque(stack) referenced by 'key'. + * + * @param key identifies the appropriate stack + * @param value the value to push into the stack + * @since 2.0.0 + */ + public void pushByKey(String key, String value); + + /** + * Pop the stack referenced by 'key' and return the value possibly null. + * + * @param key identifies the deque(stack) + * @return the value just popped. May be null/ + * @since 2.0.0 + */ + public String popByKey(String key); + + /** + * Returns a copy of the deque(stack) referenced by 'key'. May be null. + * + * @param key identifies the stack + * @return copy of stack referenced by 'key'. May be null. + * + * @since 2.0.0 + */ + public Deque getCopyOfDequeByKey(String key); + + + /** + * Clear the deque(stack) referenced by 'key'. + * + * @param key identifies the stack + * + * @since 2.0.0 + */ + public void clearDequeByKey(String key); + +} diff --git a/src/main/java/org/slf4j/spi/MarkerFactoryBinder.java b/src/main/java/org/slf4j/spi/MarkerFactoryBinder.java new file mode 100644 index 0000000..0502ffa --- /dev/null +++ b/src/main/java/org/slf4j/spi/MarkerFactoryBinder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.slf4j.spi; + +import org.slf4j.IMarkerFactory; + +/** + * An internal interface which helps the static {@link org.slf4j.MarkerFactory} + * class bind with the appropriate {@link IMarkerFactory} instance. + * + * @author Ceki Gülcü + * @deprecated + */ +public interface MarkerFactoryBinder { + + /** + * Return the instance of {@link IMarkerFactory} that + * {@link org.slf4j.MarkerFactory} class should bind to. + * + * @return the instance of {@link IMarkerFactory} that + * {@link org.slf4j.MarkerFactory} class should bind to. + */ + public IMarkerFactory getMarkerFactory(); + + /** + * The String form of the {@link IMarkerFactory} object that this + * MarkerFactoryBinder instance is intended to return. + * + *

This method allows the developer to interrogate this binder's intention + * which may be different from the {@link IMarkerFactory} instance it is able to + * return. Such a discrepancy should only occur in case of errors. + * + * @return the class name of the intended {@link IMarkerFactory} instance + */ + public String getMarkerFactoryClassStr(); +} diff --git a/src/main/java/org/slf4j/spi/NOPLoggingEventBuilder.java b/src/main/java/org/slf4j/spi/NOPLoggingEventBuilder.java new file mode 100644 index 0000000..e356b47 --- /dev/null +++ b/src/main/java/org/slf4j/spi/NOPLoggingEventBuilder.java @@ -0,0 +1,100 @@ +package org.slf4j.spi; + +import java.util.function.Supplier; + +import org.slf4j.Marker; +import org.slf4j.event.Level; + +/** + *

A no-operation implementation of {@link LoggingEventBuilder}.

+ * + *

As the name indicates, the method in this class do nothing, except when a return value is expected + * in which case a singleton, i.e. the unique instance of this class is returned. + *

Returns the singleton instance of this class. + * Used by {@link org.slf4j.Logger#makeLoggingEventBuilder(Level) makeLoggingEventBuilder(Level)}.

+ * + * @return the singleton instance of this class + */ + public static LoggingEventBuilder singleton() { + return SINGLETON; + } + + @Override + public LoggingEventBuilder addMarker(Marker marker) { + return singleton(); + } + + @Override + public LoggingEventBuilder addArgument(Object p) { + return singleton(); + } + + @Override + public LoggingEventBuilder addArgument(Supplier objectSupplier) { + return singleton(); + } + + @Override + public LoggingEventBuilder addKeyValue(String key, Object value) { + return singleton(); + } + + @Override + public LoggingEventBuilder addKeyValue(String key, Supplier value) { + return singleton(); + } + + @Override + public LoggingEventBuilder setCause(Throwable cause) { + return singleton(); + } + + @Override + public void log() { + } + + @Override + public LoggingEventBuilder setMessage(String message) { + return this; + } + @Override + public LoggingEventBuilder setMessage(Supplier messageSupplier) { + return this; + } + + @Override + public void log(String message) { + } + + @Override + public void log(Supplier messageSupplier) { + } + + @Override + public void log(String message, Object arg) { + } + + @Override + public void log(String message, Object arg0, Object arg1) { + } + + @Override + public void log(String message, Object... args) { + + } + +} diff --git a/src/main/java/org/slf4j/spi/SLF4JServiceProvider.java b/src/main/java/org/slf4j/spi/SLF4JServiceProvider.java new file mode 100644 index 0000000..8e79154 --- /dev/null +++ b/src/main/java/org/slf4j/spi/SLF4JServiceProvider.java @@ -0,0 +1,60 @@ +package org.slf4j.spi; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * This interface based on {@link java.util.ServiceLoader} paradigm. + * + *

It replaces the old static-binding mechanism used in SLF4J versions 1.0.x to 1.7.x. + * + * @author Ceki G¨lc¨ + * @since 1.8 + */ +public interface SLF4JServiceProvider { + + /** + * Return the instance of {@link ILoggerFactory} that + * {@link LoggerFactory} class should bind to. + * + * @return instance of {@link ILoggerFactory} + */ + public ILoggerFactory getLoggerFactory(); + + /** + * Return the instance of {@link IMarkerFactory} that + * {@link org.slf4j.MarkerFactory} class should bind to. + * + * @return instance of {@link IMarkerFactory} + */ + public IMarkerFactory getMarkerFactory(); + + /** + * Return the instance of {@link MDCAdapter} that + * {@link MDC} should bind to. + * + * @return instance of {@link MDCAdapter} + */ + public MDCAdapter getMDCAdapter(); + + /** + * Return the maximum API version for SLF4J that the logging + * implementation supports. + * + *

For example: {@code "2.0.1"}. + * + * @return the string API version. + */ + public String getRequestedApiVersion(); + + /** + * Initialize the logging back-end. + * + *

WARNING: This method is intended to be called once by + * {@link LoggerFactory} class and from nowhere else. + * + */ + public void initialize(); +} diff --git a/src/main/java/org/slf4j/spi/package.html b/src/main/java/org/slf4j/spi/package.html new file mode 100644 index 0000000..13ea892 --- /dev/null +++ b/src/main/java/org/slf4j/spi/package.html @@ -0,0 +1,8 @@ + + + + +Classes and interfaces which are internal to SLF4J. Under most +circumstances SLF4J users should be oblivious even to the existence of +this package. + \ No newline at end of file diff --git a/src/main/resources/bungee.yml b/src/main/resources/bungee.yml new file mode 100644 index 0000000..13fca5b --- /dev/null +++ b/src/main/resources/bungee.yml @@ -0,0 +1,6 @@ +name: AuMcBot +main: com.yaohun.mcbot.McBot +version: 1.2.0 +author: Yao_Hun +depends: + - BungeePassage \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..81ee8fd --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,13 @@ +MYSQL: + Host: "127.0.0.1" + Port: "3306" + Database: "qqbind" + Users: root + Password: "o@mM6zpK#WU2m4R#eBlQyp" +Group-Numbers: + - 368488340 + - 787454040 + - 499991070 +Admin: + - '妖魂吖OP' +Group_Info: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \ No newline at end of file