Compare commits

...

9 Commits

3 changed files with 335 additions and 226 deletions

View File

@ -543,7 +543,7 @@ public class SybilRenderer {
if (p < minDisplay)
break; // sorted
buf.append("<p class=\"threatpoints\"><b>Threat Points: " + fmt.format(p) + "</b></p><ul>");
List<String> reasons = pp.getReasons();
List<String> reasons = new ArrayList<>(pp.getReasons().keySet());
if (reasons.size() > 1)
Collections.sort(reasons, rcomp);
for (String s : reasons) {

View File

@ -118,10 +118,16 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
public static final long DEFAULT_FREQUENCY = 24 * 60 * 60 * 1000L;
public static final float MIN_BLOCK_POINTS = 12.01f;
private static final byte[] IPV6_LOCALHOST = new byte[16];
static { IPV6_LOCALHOST[15] = 1; }
static {
IPV6_LOCALHOST[15] = 1;
}
// i2pd bug 64:ff9b::/96
private static final byte[] IPV6_NAT64 = new byte[16];
static { IPV6_NAT64[1] = 0x64; IPV6_NAT64[2] = (byte) 0xff; IPV6_NAT64[3] = (byte) 0x9b; }
static {
IPV6_NAT64[1] = 0x64;
IPV6_NAT64[2] = (byte) 0xff;
IPV6_NAT64[3] = (byte) 0x9b;
}
/** Get via getInstance() */
private Analysis(RouterContext ctx, ClientAppManager mgr, String[] args) {
@ -149,7 +155,9 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
return rv;
}
public PersistSybil getPersister() { return _persister; }
public PersistSybil getPersister() {
return _persister;
}
/**
* Load the persisted blocklist and tell the router
@ -157,9 +165,13 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
* @since 0.9.50
*/
private class InitJob extends JobImpl {
public InitJob() { super(_context); }
public InitJob() {
super(_context);
}
public String getName() { return "Load Sybil Blocklist"; }
public String getName() {
return "Load Sybil Blocklist";
}
public void runJob() {
Map<String, Long> map = _persister.readBlocklist();
@ -234,6 +246,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
/**
* ClientApp interface
*
* @param args ignored
*/
public synchronized void shutdown(String[] args) {
@ -289,10 +302,12 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
private static class RouterInfoRoutingKeyComparator implements Comparator<RouterInfo>, Serializable {
private final Hash _us;
/** @param us ROUTING KEY */
public RouterInfoRoutingKeyComparator(Hash us) {
_us = us;
}
public int compare(RouterInfo l, RouterInfo r) {
return HashDistance.getDistance(_us, l.getHash()).compareTo(HashDistance.getDistance(_us, r.getHash()));
}
@ -303,19 +318,20 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
* points1 is unmodified.
*/
/****
private void mergePoints(Map<Hash, Points> points1, Map<Hash, Points> points2) {
for (Map.Entry<Hash, Points> e : points1.entrySet()) {
Hash h = e.getKey();
Points p1 = e.getValue();
Points p2 = points2.get(h);
if (p2 != null) {
p2.points += p1.points;
p2.reasons.addAll(p1.reasons);
} else {
points2.put(h, p1);
}
}
}
* private void mergePoints(Map<Hash, Points> points1, Map<Hash, Points>
* points2) {
* for (Map.Entry<Hash, Points> e : points1.entrySet()) {
* Hash h = e.getKey();
* Points p1 = e.getValue();
* Points p2 = points2.get(h);
* if (p2 != null) {
* p2.points += p1.points;
* p2.reasons.addAll(p1.reasons);
* } else {
* points2.put(h, p1);
* }
* }
* }
****/
/** */
@ -330,6 +346,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
/**
* All the floodfills, not including us
*
* @since 0.9.38 split out from renderRouterInfoHTML
*/
public List<RouterInfo> getFloodfills(Hash us) {
@ -347,6 +364,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
/**
* All the routers, not including us
*
* @since 0.9.41
*/
public List<RouterInfo> getAllRouters(Hash us) {
@ -376,6 +394,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
/**
* Analyze threats. No output.
* Return separate maps for each cause instead?
*
* @param includeAll false for floodfills only
* @since 0.9.38
*/
@ -440,7 +459,8 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
continue;
Hash rkey = ls.getRoutingKey();
TunnelPool in = clientInboundPools.get(client);
String name = (in != null) ? DataHelper.escapeHTML(in.getSettings().getDestinationNickname()) : client.toBase64().substring(0,4);
String name = (in != null) ? DataHelper.escapeHTML(in.getSettings().getDestinationNickname())
: client.toBase64().substring(0, 4);
// closest to routing key today
calculateRouterInfo(rkey, name, ris, points);
// closest to routing key tomorrow
@ -460,13 +480,19 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
* @since 0.9.57
*/
private static class DummyList extends ArrayList<RouterInfo> {
public DummyList() { super(0); }
public DummyList() {
super(0);
}
@Override
public boolean add(RouterInfo ri) { return true; }
public boolean add(RouterInfo ri) {
return true;
}
}
/**
* Blocklist and Banlist if configured
*
* @since 0.9.41
*/
private void doBlocking(Map<Hash, Points> points) {
@ -474,10 +500,12 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
long now = _context.clock().now();
long blockUntil = _context.getProperty(Analysis.PROP_BLOCKTIME, DEFAULT_BLOCK_TIME) + now;
try {
threshold = Double.parseDouble(_context.getProperty(PROP_THRESHOLD, Double.toString(DEFAULT_BLOCK_THRESHOLD)));
threshold = Double
.parseDouble(_context.getProperty(PROP_THRESHOLD, Double.toString(DEFAULT_BLOCK_THRESHOLD)));
if (threshold < MIN_BLOCK_POINTS)
threshold = MIN_BLOCK_POINTS;
} catch (NumberFormatException nfe) {}
} catch (NumberFormatException nfe) {
}
String day = DataHelper.formatTime(now);
Set<String> blocks = new HashSet<String>();
for (Map.Entry<Hash, Points> e : points.entrySet()) {
@ -552,10 +580,10 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
break; // sorted;
point *= PAIR_DISTANCE_FACTOR;
String b2 = p.r2.getHash().toBase64();
addPoints(points, p.r1.getHash(), point, "Very close (" + fmt.format(distance) +
addPoints(points, p.r1.getHash(), point, Points.REASON_TOO_CLOSE + " (" + fmt.format(distance) +
") to other " + other + " <a href=\"netdb?r=" + b2 + "\">" + b2 + "</a>");
String b1 = p.r1.getHash().toBase64();
addPoints(points, p.r2.getHash(), point, "Very close (" + fmt.format(distance) +
addPoints(points, p.r2.getHash(), point, Points.REASON_TOO_CLOSE + " (" + fmt.format(distance) +
") to other " + other + " <a href=\"netdb?r=" + b1 + "\">" + b1 + "</a>");
}
return avg;
@ -585,6 +613,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
/**
* v6 only
*
* @since 0.9.57
*/
private static byte[] getIPv6(RouterInfo ri) {
@ -632,15 +661,15 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
String reason48;
if (ourIP != null) {
reason32 = "Same IP as <a href=\"/netdb?ip=" +
reason32 = Points.REASON_SAME_IP4 + " as <a href=\"/netdb?ip=" +
Addresses.toString(ourIP) +
"&amp;sybil\">us</a>";
reason24 = "Same IPv4 /24 as <a href=\"/netdb?ip=" +
reason24 = Points.REASON_SAME_IP4_24 + " as <a href=\"/netdb?ip=" +
(ourIP[0] & 0xff) + '.' +
(ourIP[1] & 0xff) + '.' +
(ourIP[2] & 0xff) +
".0/24&amp;sybil\">us</a>";
reason16 = "Same IPv4 /16 as <a href=\"/netdb?ip=" +
reason16 = Points.REASON_SAME_IP4_16 + " as <a href=\"/netdb?ip=" +
(ourIP[0] & 0xff) + '.' +
(ourIP[1] & 0xff) +
".0.0/16&amp;sybil\">us</a>";
@ -650,13 +679,13 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
reason16 = null;
}
if (ourIPv6 != null) {
reason64 = "Same IPv6 /64 as <a href=\"/netdb?ip=" +
reason64 = Points.REASON_SAME_IP6_64 + " as <a href=\"/netdb?ip=" +
Integer.toString(((ourIPv6[0] << 8) & 0xff00) | (ourIPv6[1] & 0xff), 16) + ':' +
Integer.toString(((ourIPv6[2] << 8) & 0xff00) | (ourIPv6[3] & 0xff), 16) + ':' +
Integer.toString(((ourIPv6[4] << 8) & 0xff00) | (ourIPv6[5] & 0xff), 16) + ':' +
Integer.toString(((ourIPv6[6] << 8) & 0xff00) | (ourIPv6[7] & 0xff), 16) +
"::&amp;sybil\">us</a>";
reason48 = "Same IPv6 /48 as <a href=\"/netdb?ip=" +
reason48 = Points.REASON_SAME_IP6_48 + " as <a href=\"/netdb?ip=" +
Integer.toString(((ourIPv6[0] << 8) & 0xff00) | (ourIPv6[1] & 0xff), 16) + ':' +
Integer.toString(((ourIPv6[2] << 8) & 0xff00) | (ourIPv6[3] & 0xff), 16) + ':' +
Integer.toString(((ourIPv6[4] << 8) & 0xff00) | (ourIPv6[5] & 0xff), 16) +
@ -729,7 +758,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int i1 = (i >> 16) & 0xff;
int i2 = (i >> 8) & 0xff;
int i3 = i & 0xff;
String reason = "Same IP with <a href=\"/netdb?ip=" +
String reason = Points.REASON_SAME_IP4 + " with <a href=\"/netdb?ip=" +
i0 + '.' + i1 + '.' + i2 + '.' + i3 + "&amp;sybil\">" +
(count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
for (RouterInfo info : ris) {
@ -778,7 +807,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int i0 = i >> 16;
int i1 = (i >> 8) & 0xff;
int i2 = i & 0xff;
String reason = "Same IPv4 /24 with <a href=\"/netdb?ip=" +
String reason = Points.REASON_SAME_IP4_24 + " with <a href=\"/netdb?ip=" +
i0 + '.' + i1 + '.' + i2 + ".0/24&amp;sybil\">" +
(count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
for (RouterInfo info : ris) {
@ -829,7 +858,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int i = ii.intValue();
int i0 = i >> 8;
int i1 = i & 0xff;
String reason = "Same IPv4 /16 with <a href=\"/netdb?ip=" +
String reason = Points.REASON_SAME_IP4_16 + " with <a href=\"/netdb?ip=" +
i0 + '.' + i1 + ".0.0/16&amp;sybil\">" +
(count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
for (RouterInfo info : ris) {
@ -878,7 +907,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int i5 = (int) ((i >> 16) & 0xff);
int i6 = (int) ((i >> 8) & 0xff);
int i7 = (int) (i & 0xff);
String reason = "Same IPv6 /64 with <a href=\"/netdb?ip=" +
String reason = Points.REASON_SAME_IP6_64 + " with <a href=\"/netdb?ip=" +
Integer.toString((i0 << 8) | i1, 16) + ':' +
Integer.toString((i2 << 8) | i3, 16) + ':' +
Integer.toString((i4 << 8) | i5, 16) + ':' +
@ -941,7 +970,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int i3 = (int) ((i >> 16) & 0xff);
int i4 = (int) ((i >> 8) & 0xff);
int i5 = (int) (i & 0xff);
String reason = "Same IPv6 /48 with <a href=\"/netdb?ip=" +
String reason = Points.REASON_SAME_IP6_48 + " with <a href=\"/netdb?ip=" +
Integer.toString((i0 << 8) | i1, 16) + ':' +
Integer.toString((i2 << 8) | i3, 16) + ':' +
Integer.toString((i4 << 8) | i5, 16) +
@ -1001,10 +1030,13 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
if (s.equals(ourFamily)) {
if (fkc.verifyOurFamily(info)) {
point = POINTS_OUR_FAMILY;
reason = "Our family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
reason = Points.REASON_MY_FAMILY + " \"" + ss + "\" with <a href=\"/netdb?fam=" + ss
+ "&amp;sybil\">"
+ (count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
} else {
point = POINTS_BAD_OUR_FAMILY;
reason = "Spoofed our family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
reason = Points.REASON_SPOOFED_MY_FAMILY + " \"" + ss + "\" with <a href=\"/netdb?fam=" + ss
+ "&amp;sybil\">" + (count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
}
} else {
FamilyKeyCrypto.Result r = fkc.verify(info);
@ -1014,13 +1046,16 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
case INVALID_SIG:
case NO_SIG:
point = POINTS_BAD_FAMILY;
reason = "Bad family config \"" + ss + '"';
reason = Points.REASON_INVALID_FAMILY + " \"" + ss + '"';
break;
case STORED_KEY:
point = POINTS_FAMILY_VERIFIED;
if (count > 1)
reason = "In verified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
reason = Points.REASON_KNOWN_FAMILY + " \"" + ss + "\" with <a href=\"/netdb?fam="
+ ss
+ "&amp;sybil\">" + (count - 1) + " other" + ((count > 2) ? "s" : "")
+ "</a>";
else
reason = "In verified family \"" + ss + '"';
break;
@ -1034,18 +1069,23 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
default:
point = POINTS_FAMILY;
if (count > 1)
reason = "In unverified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
reason = Points.REASON_VALID_FAMILY + " \"" + ss + "\" with <a href=\"/netdb?fam="
+ ss
+ "&amp;sybil\">" + (count - 1) + " other" + ((count > 2) ? "s" : "")
+ "</a>";
else
reason = "In unverified family \"" + ss + '"';
reason = Points.REASON_VALID_FAMILY + " \"" + ss + '"';
break;
}
}
} else if (count > 1) {
point = POINTS_FAMILY;
reason = "In unverified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
reason = Points.REASON_VALID_FAMILY + " \"" + ss + "\" with <a href=\"/netdb?fam=" + ss
+ "&amp;sybil\">"
+ (count - 1) + " other" + ((count > 2) ? "s" : "") + "</a>";
} else {
point = POINTS_FAMILY;
reason = "In unverified family \"" + ss + '"';
reason = Points.REASON_VALID_FAMILY + " \"" + ss + '"';
}
addPoints(points, info.getHash(), point, reason);
}
@ -1062,7 +1102,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
for (RouterInfo info : ris) {
Hash h = info.getHash();
if (_context.banlist().isBanlisted(h)) {
StringBuilder buf = new StringBuilder("Banlisted");
StringBuilder buf = new StringBuilder(Points.REASON_BANLISTED);
Banlist.Entry entry = banEntries.get(h);
if (entry != null) {
if (entry.cause != null) {
@ -1087,7 +1127,7 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
// (POINTS_NEW / 48) for every hour under 48, max POINTS_NEW
double point = Math.min(POINTS_NEW, (2 * DAY - age) / (2 * DAY / POINTS_NEW));
addPoints(points, h, point,
"First heard about: " + _t("{0} ago", DataHelper.formatDuration2(age)));
Points.REASON_CONTACT + " " + _t("{0} ago", DataHelper.formatDuration2(age)));
}
}
DBHistory dbh = prof.getDBHistory();
@ -1100,7 +1140,8 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
if (ra.getTotalEventCount() > 0) {
double avg = 100 * ra.getAverage();
if (avg > 40)
addPoints(points, h, (avg - 40) / 6.0, "Lookup fail rate " + ((int) avg) + '%');
addPoints(points, h, (avg - 40) / 6.0,
Points.REASON_ALWAYS_FAIL_LOOKUP + " " + ((int) avg) + '%');
}
}
}
@ -1111,10 +1152,12 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
public void addVersionPoints(List<RouterInfo> ris, Map<Hash, Points> points) {
RouterInfo us = _context.router().getRouterInfo();
if (us == null) return;
if (us == null)
return;
String ourVer = us.getVersion();
// TODO do the math once we hit version 1.0.0
if (!ourVer.startsWith("0.9.")) return;
if (!ourVer.startsWith("0.9."))
return;
ourVer = ourVer.substring(4);
int dot = ourVer.indexOf('.');
if (dot > 0)
@ -1122,18 +1165,21 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int minor;
try {
minor = Integer.parseInt(ourVer);
} catch (NumberFormatException nfe) { return; }
} catch (NumberFormatException nfe) {
return;
}
for (RouterInfo info : ris) {
Hash h = info.getHash();
String caps = info.getCapabilities();
if (!caps.contains("R"))
addPoints(points, h, POINTS_UNREACHABLE, "Unreachable: " + DataHelper.escapeHTML(caps));
addPoints(points, h, POINTS_UNREACHABLE, Points.REASON_UNREACHABLE + DataHelper.escapeHTML(caps));
if (!caps.contains("f"))
addPoints(points, h, POINTS_NONFF, "Non-floodfill");
addPoints(points, h, POINTS_NONFF, Points.REASON_NON_FLOODFILL);
String hisFullVer = info.getVersion();
if (!hisFullVer.startsWith("0.9.")) {
if (!hisFullVer.startsWith("1."))
addPoints(points, h, POINTS_BAD_VERSION, "Strange version " + DataHelper.escapeHTML(hisFullVer));
addPoints(points, h, POINTS_BAD_VERSION,
Points.REASON_BAD_VERSION + DataHelper.escapeHTML(hisFullVer));
// TODO do the math once we hit version 1.0.0
continue;
}
@ -1144,11 +1190,14 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
int hisMinor;
try {
hisMinor = Integer.parseInt(hisVer);
} catch (NumberFormatException nfe) { continue; }
} catch (NumberFormatException nfe) {
continue;
}
int howOld = minor - hisMinor;
if (howOld < 3)
continue;
addPoints(points, h, howOld * VERSION_FACTOR, howOld + " versions behind: " + DataHelper.escapeHTML(hisFullVer));
addPoints(points, h, howOld * VERSION_FACTOR,
Points.REASON_OLD_VERSION + howOld + " versions behind: " + DataHelper.escapeHTML(hisFullVer));
}
}
@ -1172,13 +1221,15 @@ public class Analysis extends JobImpl implements RouterApp, Runnable {
if (point <= 0)
break;
point *= OUR_KEY_FACTOR;
addPoints(points, ri.getHash(), point, "Very close (" + fmt.format(dist) + ") to our key " + usName + ": " + us.toBase64());
addPoints(points, ri.getHash(), point,
Points.REASON_TOO_CLOSE + "(" + fmt.format(dist) + ") to our key " + usName + ": " + us.toBase64());
}
}
/**
* For debugging
* http://forums.sun.com/thread.jspa?threadID=597652
*
* @since 0.7.14
*/
private static double biLog2(BigInteger a) {

View File

@ -3,6 +3,8 @@ package net.i2p.router.sybil;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.DataHelper;
@ -12,18 +14,42 @@ import net.i2p.data.DataHelper;
* @since 0.9.38 moved from SybilRenderer
*/
public class Points implements Comparable<Points> {
private double points;
private final List<String> reasons;
private final Map<String, Double> reasons;
public static final String REASON_NON_FLOODFILL = "Non-floodfill: ";
public static final String REASON_TOO_CLOSE = "Very close: ";
public static final String REASON_ALWAYS_FAIL_LOOKUP = "Lookup fail rate: ";
public static final String REASON_UNREACHABLE = "Unreachable: ";
public static final String REASON_BAD_VERSION = "Strange Version: ";
public static final String REASON_OLD_VERSION = "Old Version: ";
public static final String REASON_MY_FAMILY = "Our family: ";
public static final String REASON_SPOOFED_MY_FAMILY = "Spoofed my family: ";
public static final String REASON_KNOWN_FAMILY = "Verified family: ";
public static final String REASON_INVALID_FAMILY = "Invalid family: ";
public static final String REASON_VALID_FAMILY = "Valid Unverified family: ";
public static final String REASON_SAME_IP4 = "Same IPv4: ";
public static final String REASON_SAME_IP4_16 = "Same IPv4/16: ";
public static final String REASON_SAME_IP4_24 = "Same IPv4/24: ";
public static final String REASON_SAME_IP6 = "Same IPv6: ";
public static final String REASON_SAME_IP6_48 = "Same IPv6/48: ";
public static final String REASON_SAME_IP6_64 = "Same IPv6/64: ";
public static final String REASON_BANLISTED = "Banlisted: ";
public static final String REASON_CONTACT = "First Heard About: ";
/**
* @since 0.9.38
*/
private Points() {
reasons = new ArrayList<String>(4);
reasons = new ConcurrentHashMap<String, Double>(4);
}
/**
* @param reason may not contain '%'
* @param reason may not contain '%' or '\t'
*/
public Points(double d, String reason) {
this();
@ -31,32 +57,64 @@ public class Points implements Comparable<Points> {
}
/**
* @since 0.9.38
* Compare 2 reason strings by comparing them to the list of static constants above.
* Ignores anything following the `:` when comparing the strings.
*
* @param comparator
* @param base
* @return
*/
public double getPoints() {
return points;
public static boolean compareReason(String comparator, String base) {
String[] comparatorPrefix = DataHelper.split(comparator, ":");
String[] basePrefix = DataHelper.split(base, ":");
if (comparatorPrefix == null)
return false;
if (basePrefix == null)
return false;
return basePrefix[0].toLowerCase().equals(comparatorPrefix[0].toLowerCase());
}
private double points() {
double rv = 0;
for (String reason : reasons.keySet()) {
rv += reasons.get(reason);
}
return rv;
}
/**
* @since 0.9.38
*/
public List<String> getReasons() {
public double getPoints() {
return points();
}
/**
* @since 0.9.38
*/
public Map<String, Double> getReasons() {
return reasons;
}
/**
* @param reason may not contain '%'
* @param reason may not contain '%' or '\t'
* @since 0.9.38
*/
public void addPoints(double d, String reason) {
points += d;
DecimalFormat format = new DecimalFormat("#0.00");
String rsn = format.format(d) + ": " + reason;
reasons.add(rsn);
Double rp = reasons.get(rsn);
if (rp == null) {
// reason was not yet present in the map, create a new entry for it.
reasons.put(rsn, d);
} else {
// reason was present in the map, add the points to it.
rp += d;
}
}
public int compareTo(Points r) {
return Double.compare(points, r.points);
return Double.compare(points(), r.points());
}
/**
@ -71,28 +129,32 @@ public class Points implements Comparable<Points> {
/**
* For persistence.
* Total points and reasons, '%' separated, no newline.
* points and reasons, '%' separated, new line between pairs.
* The separation character is chosen to not conflict with
* decimal point in various locales, or chars in reasons, including HTML links,
* or special chars in Pattern.
*
* Format changed in 0.9.64
*
* @since 0.9.38
*/
public void toString(StringBuilder buf) {
buf.append(points);
for (String r : reasons) {
buf.append('%').append(r.replace("%", "&#x25;"));
for (String r : reasons.keySet()) {
buf.append(reasons.get(r)).append('%').append(r.replace("%", "&#x25;")).append("\t");
}
}
/**
* For persistence.
*
* @return null on failure
* @since 0.9.38
*/
public static Points fromString(String s) {
String[] ss = DataHelper.split(s, "%");
if (ss.length < 2)
Points rv = new Points();
for (String lineString : DataHelper.split(s, "\t")) {
String[] ss = DataHelper.split(lineString, "%");
if (ss.length != 2)
return null;
double d;
try {
@ -100,12 +162,8 @@ public class Points implements Comparable<Points> {
} catch (NumberFormatException nfe) {
return null;
}
Points rv = new Points();
for (int i = 1; i < ss.length; i++) {
rv.reasons.add(ss[i]);
rv.reasons.put(ss[1], d);
}
rv.points = d;
return rv;
}
}