package chatty.util;
import chatty.Helper;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Decodes and encodes IRCv3 message tags and provides convenience methods to
* access them.
*
* The data of MsgTags objects is read-only. Use the factory methods to create
* new instances.
*
* @author tduva
*/
public class MsgTags {
private static final Map<String, String> EMPTY_TAGS = new HashMap<>();
/**
* Empty MsgTags object. Can be used if no tags should be provided, to still
* have a MsgTags object.
*/
public static final MsgTags EMPTY = new MsgTags(null);
private final Map<String, String> tags;
private MsgTags(Map<String, String> tags) {
if (tags == null) {
this.tags = EMPTY_TAGS;
} else {
this.tags = tags;
}
}
/**
* Returns true if the given key is contained in these tags.
*
* @param key The key to look for
* @return true if the key is in the tags, false otherwise
*/
public boolean containsKey(String key) {
return tags.containsKey(key);
}
/**
* Returns true if there are no key/value pairs.
*
* @return true if empty
*/
public boolean isEmpty() {
return tags.isEmpty();
}
/**
* Returns true if the given key in the tags is equal to "1", false
* otherwise.
*
* @param key The key to check
* @return True if equal to 1, false otherwise
*/
public boolean isTrue(String key) {
return "1".equals(tags.get(key));
}
public boolean isValue(String key, String value) {
return value.equals(tags.get(key));
}
public boolean isEmpty(String key) {
return !tags.containsKey(key) || tags.get(key).isEmpty();
}
/**
* Returns the String associated with key, or null if the key doesn't exist.
*
* @param key The key to look up in the tags
* @return String associated with this key, or null
*/
public String get(String key) {
return get(key, null);
}
/**
* Returns the String associated with key, or the defaultValue if the key
* doesn't exist.
*
* @param key The key to look up in the tags
* @param defaultValue The default value to return if key isn't in tags
* @return String associated with this key, or defaultValue
*/
public String get(String key, String defaultValue) {
if (tags.containsKey(key)) {
return tags.get(key);
}
return defaultValue;
}
/**
* Returns the integer associated with the given key, or the defaultValue if
* no integer was found for that key.
*
* @param key The key to retrieve the value for
* @param defaultValue The default value to return if the given key doesn't
* point to an integer value
* @return The integer associated with the key, or defaultValue
*/
public int getInteger(String key, int defaultValue) {
if (tags.get(key) != null) {
try {
return Integer.parseInt(tags.get(key));
} catch (NumberFormatException ex) {
// Just go to default value
}
}
return defaultValue;
}
/**
* Returns the long associated with the given key, or the defaultValue if no
* long was found for that key.
*
* @param key The key to retrieve the value for
* @param defaultValue The default value to return if the given key doesn't
* point to a long value
* @return The long associated with the key, or defaultValue
*/
public long getLong(String key, long defaultValue) {
if (tags.get(key) != null) {
try {
return Long.parseLong(tags.get(key));
} catch (NumberFormatException ex) {
// Just go to default value
}
}
return defaultValue;
}
/**
* Build a IRCv3 tags String for this tags object (no leading @).
*
* @return The tags string (may be empty if this tags object is empty)
*/
public String toTagsString() {
StringBuilder b = new StringBuilder();
Iterator<String> it = tags.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
String value = tags.get(key);
if (isValidKey(key)) {
b.append(key);
if (isValidValue(value)) {
b.append("=").append(escapeValue(value));
}
if (it.hasNext()) {
b.append(";");
}
}
}
return b.toString();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final MsgTags other = (MsgTags) obj;
if (!Objects.equals(this.tags, other.tags)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 5;
hash = 53 * hash + Objects.hashCode(this.tags);
return hash;
}
private static final Pattern KEY_PATTERN = Pattern.compile("[a-z0-9-]+", Pattern.CASE_INSENSITIVE);
private static boolean isValidKey(String key) {
return KEY_PATTERN.matcher(key).matches();
}
private static boolean isValidValue(String value) {
return value != null;
}
private static String escapeValue(String value) {
return Helper.tagsvalue_encode(value);
}
@Override
public String toString() {
return tags.toString();
}
//================
// Factory Methods
//================
/**
* Parse the given IRCv3 tags String (no leading @) into a MsgTags object.
*
* @param tags The tags String
* @return MsgTags object, empty if tags was null
*/
public static MsgTags parse(String tags) {
Map<String, String> parsedTags = parseTags(tags);
if (parsedTags == null) {
return EMPTY;
}
return new MsgTags(parsedTags);
}
/**
* Create a new MsgTags object with the given key/value pairs.
*
* @param args Alternating key/value pairs
* @return MsgTags object
*/
public static MsgTags create(String... args) {
Map<String, String> tags = new HashMap<>();
Iterator<String> it = Arrays.asList(args).iterator();
while (it.hasNext()) {
String key = it.next();
if (it.hasNext()) {
tags.put(key, it.next());
} else {
tags.put(key, null);
}
}
return new MsgTags(tags);
}
private static Map<String, String> parseTags(String data) {
if (data == null) {
return null;
}
String[] tags = data.split(";");
if (tags.length > 0) {
Map<String, String> result = new HashMap<>();
for (String tag : tags) {
String[] keyValue = tag.split("=",2);
if (keyValue.length == 2) {
result.put(keyValue[0], Helper.tagsvalue_decode(keyValue[1]));
} else if (!keyValue[0].isEmpty()) {
result.put(keyValue[0], null);
}
}
return result;
}
return null;
}
}