/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.addthis.basis.kv; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import com.addthis.basis.util.LessBytes; import com.addthis.basis.util.LessNumbers; import com.addthis.basis.util.LessStrings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class that represents a collection of key/value pairs. * * What was going on when this class was implemented? * @deprecated Consider using the * <a href="http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/Multimap.html">Multimap</a> * interface. */ @Deprecated public class KVPairs implements Iterable<KVPair> { protected static final Logger log = LoggerFactory.getLogger(KVPairs.class); private static final boolean putupper = System.getProperty("kv.putupper", "1").equals("1"); private static final boolean getupper = System.getProperty("kv.getupper", "1").equals("1"); public static KVPairs mergeAndCopy(KVPairs base, KVPairs overrider) { if (overrider == null) { return base; } else if (base == null) { return overrider; } KVPairs ret = overrider.getCopy(); for (KVPair kv : base) { if (!ret.hasKey(kv.getKey())) { ret.addValue(kv.getKey(), kv.getValue()); } } return ret; } public static KVPairs fromFullURL(String url) { KVPairs kvp = new KVPairs(); kvp.addValue("_URLROOT_", kvp.parseURLPath(url, true)); return kvp; } /* * Remove pairs with empty/null values */ public static KVPairs clearEmpties(KVPairs values) { for (Iterator<KVPair> iter = values.elements(); iter.hasNext();) { KVPair kv = iter.next(); if (LessStrings.isEmpty(kv.getValue())) { iter.remove(); } } return values; } private String fixGetCase(String key) { return getupper ? key.toUpperCase() : key; } private HashMap<String, KVPair> pairs; /** * Constructs an empty KVPairs object. */ public KVPairs() { pairs = new LinkedHashMap<>(); } protected KVPairs(KVPairs kv) { this.pairs = kv.pairs; } /** * Constructs a KVPairs object from an urlencoded string. * * @param urlpath */ public KVPairs(String urlpath) { this(); if (urlpath != null) { parseURLPath(urlpath, false); } } public KVPairs(byte kbin[]) { this(); fromBinArray(kbin); } /** * Returns a shallow clone of this KVPairs. */ @SuppressWarnings("unchecked") public KVPairs getCopy() { KVPairs kv = new KVPairs(this); try { kv.pairs = (HashMap<String, KVPair>) pairs.clone(); } catch (Exception ex) { log.trace("getCopy", ex); } return kv; } /** * Returns a more human readable (but not always decodable copy) for debugging. */ public String getPrintable() { return getPrintable(false); } public String getPrintable(boolean newline) { StringBuilder sb = new StringBuilder(); for (Iterator<KVPair> i = pairs.values().iterator(); i.hasNext();) { KVPair kv = i.next(); sb.append(kv.getKey() + "='" + kv.getValue() + "'"); if (i.hasNext()) { sb.append(newline ? '\n' : ' '); } } return sb.toString(); } private void parsePair(String s) { KVPair kv = KVPair.parsePair(s); if (kv != null) { addPair(kv); } } /** * A faster tokenizer than StringTokenizer (about 2x) */ private void parsePairs(String s, char del) { if (s.length() == 0) { return; } int i = 0; int j = s.indexOf(del); while (j >= 0) { if (j > 0) { parsePair(s.substring(i, j)); } i = j + 1; j = s.indexOf(del, i); } parsePair(s.substring(i)); } /** * Parses a urlencoded string of key/value pairs and adds the values * to this KVPairs object. * * @param urlPath * @param lookForQ * @return */ public String parseURLPath(String urlPath, boolean lookForQ) { String filePath = urlPath; if (lookForQ) { int idx = urlPath.indexOf('?'); if (idx < 0 || idx == urlPath.length()) { return urlPath; } filePath = urlPath.substring(0, idx); urlPath = urlPath.substring(idx + 1); } parsePairs(urlPath, '&'); return filePath; } public KVPairs prefix(String prefix) { HashMap<String, KVPair> oldPairs = pairs; pairs = new LinkedHashMap<>(); for (Map.Entry<String, KVPair> mapEntry : oldPairs.entrySet()) { KVPair kvpair = mapEntry.getValue(); kvpair.setKey(prefix + kvpair.getKey()); addPair(kvpair); } return this; } /** * Merges values from provided KVPairs overriding any values * with the same keys in this KVPairs. * * @param add */ public synchronized KVPairs merge(KVPairs add) { if (add != null) { pairs.putAll(add.pairs); } return this; } /** * merge which is empty-value-aware- if there is an empty value in the target, override it with a * non-empty value form the source */ public KVPairs mergeNotEmpty(KVPairs add) { if (add != null) { for (String key : add.pairs.keySet()) { KVPair newVal = add.getPair(key); KVPair thisVal = getPair(key); if ((newVal != null) && (!LessStrings.isEmpty(newVal.getValue()))) { if ((thisVal == null) || (LessStrings.isEmpty(thisVal.getValue()))) { addPair(newVal); } } } } return this; } public KVPairs add(String key, String val) { addValue(key, val); return this; } public KVPairs add(String key, int val) { return add(key, Integer.toString(val)); } private static KVPair createPair(String key, String value) { return new KVPair(key, value); } /** * Adds a key/value pair. * * @param key * @param val * @return */ public KVPair addValue(String key, String val) { return addPair(createPair(key, val)); } public KVPair addValue(String key, int value) { return addPair(createPair(key, Integer.toString(value))); } public KVPair addValue(String key, long value) { return addPair(createPair(key, Long.toString(value))); } public KVPair addValue(String key, float value) { return addPair(createPair(key, Float.toString(value))); } public KVPair addValue(String key, double value) { return addPair(createPair(key, Double.toString(value))); } public KAPair addValue(String key, String... values) { KAPair pair = new KAPair(key, values); addPair(pair); return pair; } /** * putValue() synonym for addValue() */ public KVPair putValue(String key, String val) { return addValue(key, val); } public KVPair putValue(String key, int value) { return addValue(key, value); } public KVPair putValue(String key, long value) { return addValue(key, value); } public KVPair putValue(String key, float value) { return addValue(key, value); } public KVPair putValue(String key, double value) { return addValue(key, value); } public KAPair putValue(String key, String... values) { return addValue(key, values); } /** * Adds a key/value pair from a KVPair object. * * @param pair * @return */ public final synchronized KVPair addPair(KVPair pair) { String key = pair.getKey(); return pairs.put(putupper ? key.toUpperCase() : key, pair); } public KVPair putPair(KVPair pair) { return addPair(pair); } /** * Replaces the value for an existing key and adds a new key/value pair * for a missing key. Preserves original key and it's case. */ public final void replaceOrAdd(String key, String value) { KVPair pair = getPair(key); if (pair != null) { pair.setValue(value); } else { addValue(key, value); } } /** * Replaces the value for an existing key and adds a new key/value pair * for a missing key. Preserves original key and it's case. */ public final void replaceOrAdd(String key, long value) { KVPair pair = getPair(key); if (pair != null) { pair.setValue(Long.toString(value)); } else { addValue(key, value); } } /** * Replaces the value for an existing key and adds a new key/value pair * for a missing key. Preserves original key and it's case. */ public final void replaceOrAdd(String key, int value) { KVPair pair = getPair(key); if (pair != null) { pair.setValue(Integer.toString(value)); } else { addValue(key, value); } } /** * Changes the key associated with a value. Does nothing if the * key does not exist. * * @param okey * @param nkey * @return true if the key exists, false otherwise */ public final boolean renameValue(String okey, String nkey) { KVPair p = pairs.remove(fixGetCase(okey)); if (p != null) { p.setKey(nkey); addPair(p); return true; } return false; } /** * Remove the key and value for a given key. */ public final KVPair removePair(String key) { return pairs.remove(fixGetCase(key)); } /** * Returns a string value or null of the key is absent. * Removes any matching key and data. */ public String takeValue(String key) { KVPair p = removePair(key); return (p != null ? p.getValue() : null); } /** * Returns a string value or default of the key is absent. * Removes any matching key and data. */ public String takeValue(String key, String def) { KVPair p = removePair(key); return (p != null ? p.getValue() : def); } /** * Integer takeValue() with base10. * Removes any matching key and data. */ public int takeValue(String key, int def) { return takeValue(key, def, 10); } /** * Returns an integer value using the specified base or default * if the key is absent or value malformed. * Removes any matching key and data. */ public int takeValue(String key, int def, int base) { String val = takeValue(key); return (val != null ? LessNumbers.parseInt(val, def, base) : def); } /** * Returns an integer value using the specified base or default * if the key is absent or value malformed. * Removes any matching key and data. */ public float takeValue(String key, float def) { String val = takeValue(key); return (val != null ? LessNumbers.parseFloat(val, def) : def); } /** * Long takeValue() with base10. * Removes any matching key and data. */ public long takeValue(String key, long def) { return takeValue(key, def, 10); } /** * Returns a long value using the specified base or default * if the key is absent or value malformed. * Removes any matching key and data. */ public long takeValue(String key, long def, int base) { String val = takeValue(key); return (val != null ? LessNumbers.parseLong(val, def, base) : def); } /** * Return KVPair wrapper object for a given key. * * @param key * @return KVPair object or null if no key matches. */ public final synchronized KVPair getPair(String key) { return pairs.get(fixGetCase(key)); } /** * Returns true if a given key exists. * * @param key * @return true if key exists */ public final boolean hasKey(String key) { if (key == null) { return false; } return pairs.containsKey(fixGetCase(key)); } /** * @return true if contains a pair with same key and value as given pair (not an object comparison) */ public final boolean hasPair(KVPair pair) { return pair != null && pair.equals(getPair(pair.getKey())); } /** * Return value for a given key or null if key does not exist. * * @param key * @return value or null if key does not exist. */ public String getValue(String key) { return getValue(key, null); } /** * Return value for a given key or default if key does not exist. * * @param key * @param def default value to return if key does not exist * @return value or default if key does not exist. */ public String getValue(String key, String def) { KVPair pair = getPair(key); return (pair != null ? pair.getValue() : def); } /** * Return value for a given key or default if key does not exist. * * @param key * @param def default value to return if key does not exist * @return value or default if key does not exist. */ public int getIntValue(String key, int def) { return getIntValue(10, key, def); } /** * Return value for a given key or default if key does not exist. * * @param radix base for converting string to number * @param key * @param def default value to return if key does not exist * @return value or default if key does not exist. */ public int getIntValue(int radix, String key, int def) { KVPair pair = getPair(key); return pair != null ? LessNumbers.parseInt(pair.getValue(), def, radix) : def; } /** * Return value for a given key or default if key does not exist. * * @param key * @param def default value to return if key does not exist * @return value or default if key does not exist. */ public long getLongValue(String key, long def) { return getLongValue(10, key, def); } /** * Return value for a given key or default if key does not exist. * * @param radix base for converting string to number * @param key * @param def default value to return if key does not exist * @return value or default if key does not exist. */ public long getLongValue(int radix, String key, long def) { KVPair pair = getPair(key); return pair != null ? LessNumbers.parseLong(pair.getValue(), def, radix) : def; } public float getFloatValue(String key, float def) { String val = getValue(key); return val != null ? LessNumbers.parseFloat(val, def) : def; } public double getDoubleValue(String key, double def) { String val = getValue(key); return val != null ? LessNumbers.parseDouble(val, def) : def; } /** * Changes the value for a given key. Does not add key/value. * * @param key * @param value * @return previous value or null if the key doesn't exist */ public final String setValue(String key, String value) { String oval = null; KVPair p = getPair(key); if (p != null) { oval = p.getValue(); p.setValue(value); } return oval; } /** * @return number of KVPair objects represented. */ public final int count() { return pairs.size(); } /** * Returns an Iterator of key Strings. * * @return an Iterator of key Strings */ public final Iterator<String> keys() { return pairs.keySet().iterator(); } /** * Returns an Iterator of key Strings. * * @return an Iterator of key Strings */ public final Set<String> keySet() { return pairs.keySet(); } public final Collection<KVPair> values() { return pairs.values(); } public Iterator<KVPair> iterator() { return elements(); } /** * Returns an Iterator of KVPair elements. * * @return an Iterator of KVPair elements */ public final Iterator<KVPair> elements() { return pairs.values().iterator(); } public String toURLParams() { return toString(); } public void fromURLParams(String str) { parseURLPath(str, false); } public byte[] toBinArray() { int size = 0; ArrayList<byte[]> arr = new ArrayList<>(count() * 2); for (KVPair kv : values()) { String v = kv.getValue(); byte key[] = LessBytes.toBytes(kv.getKey()); byte val[] = v != null ? LessBytes.toBytes(kv.getValue()) : new byte[0]; size += key.length + val.length; arr.add(key); arr.add(val); } byte out[] = new byte[size + arr.size()]; size = 0; for (byte el[] : arr) { for (int i = 0; i < el.length; i++) { out[size + i] = el[i] != 0 ? el[i] : (byte) ' '; } size += el.length + 1; out[size - 1] = 0; } return out; } public KVPairs fromBinArray(byte bin[]) { int pos = 0; while (pos < bin.length) { String key = null; String val = null; for (int i = pos; i < bin.length; i++) { if (bin[i] == 0) { key = new String(bin, pos, i - pos); pos = i + 1; break; } if (i == bin.length - 1) { key = new String(bin, pos, i - pos + 1); pos = i + 1; break; } } for (int i = pos; i < bin.length; i++) { if (bin[i] == 0) { val = new String(bin, pos, i - pos); pos = i + 1; break; } if (i == bin.length - 1) { val = new String(bin, pos, i - pos + 1); pos = i + 1; break; } } addValue(key, val); } return this; } /** * Returns a urlencoded string comprised of all the "name"=value&"name"=value pairs. */ public synchronized String toQuotedKeyString() { StringBuilder sb = new StringBuilder(); for (Iterator<KVPair> i = pairs.values().iterator(); i.hasNext();) { sb.append(i.next().toQuotedKeyString()); if (i.hasNext()) { sb.append("&"); } } return sb.toString(); } /** * Returns a urlencoded string comprised of all the name=value&name=value pairs. */ public synchronized String toString() { StringBuilder sb = new StringBuilder(); for (Iterator<KVPair> i = pairs.values().iterator(); i.hasNext();) { sb.append(i.next().toString()); if (i.hasNext()) { sb.append("&"); } } return sb.toString(); } /** * basic equals function, fast-fail, can be optimized for speed by iterating through keyset only once * TODO: override hashcode() method as recommended by java api * * @return true if the other KVPairs set has the equal keys and values */ public synchronized boolean equals(KVPairs other) { if (!keySet().equals(other.keySet())) { return false; } // compare key sets (tests for extra/missing pairs in other set) for (String key : keySet()) { if (!getPair(key).equals(other.getPair(key))) { return false; } // compare pairs } return true; } /** * multi-pair convenience constructors */ public KVPairs(String ka, String va) { this(); addValue(ka, va); } public KVPairs(String ka, String va, String kb, String vb) { this(); addValue(ka, va); addValue(kb, vb); } public KVPairs(String ka, String va, String kb, String vb, String kc, String vc) { this(); addValue(ka, va); addValue(kb, vb); addValue(kc, vc); } public KVPairs(String ka, String va, String kb, String vb, String kc, String vc, String kd, String vd) { this(); addValue(ka, va); addValue(kb, vb); addValue(kc, vc); addValue(kd, vd); } }