IdRange.java
/*
* Copyright (c) 2014 Wael Chatila / Icegreen Technologies. All Rights Reserved.
* This software is released under the Apache license 2.0
* This file has been modified by the copyright holder.
* Original file can be found at http://james.apache.org
*/
package com.icegreen.greenmail.imap.commands;
import java.util.*;
import java.util.regex.Pattern;
/**
* Represents a range of UID values.
* <p>
* From https://tools.ietf.org/html/rfc3501 :
* <p>
* seq-number = nz-number / "*"
* ; message sequence number (COPY, FETCH, STORE
* ; commands) or unique identifier (UID COPY,
* ; UID FETCH, UID STORE commands).
* ; * represents the largest number in use. In
* ; the case of message sequence numbers, it is
* ; the number of messages in a non-empty mailbox.
* ; In the case of unique identifiers, it is the
* ; unique identifier of the last message in the
* ; mailbox or, if the mailbox is empty, the
* ; mailbox's current UIDNEXT value.
* ; The server should respond with a tagged BAD
* ; response to a command that uses a message
* ; sequence number greater than the number of
* ; messages in the selected mailbox. This
* ; includes "*" if the selected mailbox is empty.
* <p>
* seq-range = seq-number ":" seq-number
* ; two seq-number values and all values between
* ; these two regardless of order.
* ; Example: 2:4 and 4:2 are equivalent and indicate
* ; values 2, 3, and 4.
* ; Example: a unique identifier sequence range of
* ; 3291:* includes the UID of the last message in
* ; the mailbox, even if that value is less than 3291.
* <p>
* sequence-set = (seq-number / seq-range) *("," sequence-set)
* ; set of seq-number values, regardless of order.
* ; Servers MAY coalesce overlaps and/or execute the
* ; sequence in any order.
* ; Example: a message sequence number set of
* ; 2,4:7,9,12:* for a mailbox with 15 messages is
* ; equivalent to 2,4,5,6,7,9,12,13,14,15
* ; Example: a message sequence number set of *:4,5:7
* ; for a mailbox with 10 messages is equivalent to
* ; 10,9,8,7,6,5,4,5,6,7 and MAY be reordered and
* ; overlap coalesced to be 4,5,6,7,8,9,10.
*/
public class IdRange {
private static final String PATTERN_SEQ_NUMBER = "(\\*|\\d+)";
private static final String PATTERN_SEQ_RANGE = "(" + PATTERN_SEQ_NUMBER + ":" + PATTERN_SEQ_NUMBER + ")";
private static final String PATTERN_SEQ_NUM_OR_RANGE = "(" + PATTERN_SEQ_NUMBER + "|" + PATTERN_SEQ_RANGE + ")";
private static final String PATTERN_SEQ_SET = PATTERN_SEQ_NUM_OR_RANGE + "(,(" + PATTERN_SEQ_NUM_OR_RANGE +"))*";
/**
* Matches a sequence of a single id or id range
*/
public static final Pattern SEQUENCE = Pattern.compile(PATTERN_SEQ_SET);
public static final long VALUE_WILDCARD = Long.MAX_VALUE;
private final long lowVal;
private final long highVal;
public IdRange(long singleVal) {
lowVal = singleVal;
highVal = singleVal;
}
public IdRange(long lowVal, long highVal) {
this.lowVal = lowVal;
this.highVal = highVal;
}
public long getLowVal() {
return lowVal;
}
public long getHighVal() {
return highVal;
}
public boolean includes(long uid) {
return lowVal <= uid && uid <= highVal;
}
/**
* Parses an uid sequence, a comma separated list of uid ranges.
* Note that the wildcard '*' denotes the largest number in use.
* <p/>
* Example: 1,2:5,8:*
*
* @param idRangeSequence the sequence
* @return a list of ranges, never null.
*/
public static List<IdRange> parseRangeSequence(String idRangeSequence) {
StringTokenizer tokenizer = new StringTokenizer(idRangeSequence, ",");
List<IdRange> ranges = new ArrayList<>();
while (tokenizer.hasMoreTokens()) {
ranges.add(parseRange(tokenizer.nextToken()));
}
return ranges;
}
/**
* Parses a single id range, eg "1" or "1:2" or "4:*".
*
* @param range the range.
* @return the parsed id range.
*/
public static IdRange parseRange(String range) {
int pos = range.indexOf(':');
try {
if (pos == -1) {
long value = parseLong(range);
return new IdRange(value);
} else {
long lowVal = parseLong(range.substring(0, pos));
long highVal = parseLong(range.substring(pos + 1));
// two seq-number values and all values between these two regardless of order
// 2:4 is equivalent to 4:2
if (lowVal > highVal) {
return new IdRange(highVal, lowVal);
} else {
return new IdRange(lowVal, highVal);
}
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid message set " + range);
}
}
public static IdRange[] convertUidsToIdRangeArray(List<Long> uids) {
if (uids == null || uids.isEmpty()) {
return new IdRange[0];
}
List<Long> uidsLocal = new LinkedList<>(uids);
Collections.sort(uidsLocal);
List<IdRange> ids = new LinkedList<>();
IdRange currentIdRange = new IdRange(uidsLocal.get(0));
for (Long uid : uidsLocal) {
if (uid > currentIdRange.getHighVal() && (uid == currentIdRange.getHighVal() + 1)) {
currentIdRange = new IdRange(currentIdRange.getLowVal(), uid);
} else {
ids.add(currentIdRange);
currentIdRange = new IdRange(uid);
}
}
if (!ids.contains(currentIdRange)) {
ids.add(currentIdRange);
}
return ids.toArray(new IdRange[0]);
}
public static String uidsToRangeString(List<Long> uids) {
return idRangesToString(convertUidsToIdRangeArray(uids));
}
public static String idRangeToString(IdRange idRange) {
return idRange.getHighVal() == idRange.getLowVal()
? Long.toString(idRange.getLowVal())
: idRange.getLowVal() + ":" + idRange.getHighVal();
}
public static String idRangesToString(IdRange[] idRanges) {
StringBuilder sb = new StringBuilder();
for (IdRange idRange : idRanges) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(idRangeToString(idRange));
}
return sb.toString();
}
private static long parseLong(String value) {
if (value.length() == 1 && value.charAt(0) == '*') {
return VALUE_WILDCARD;
}
return Long.parseLong(value);
}
/**
* Checks if ranges contain the uid
*
* @param idRanges the id ranges
* @param uid the uid
* @return true, if ranges contain given uid
*/
public static boolean containsUid(IdRange[] idRanges, long uid) {
if (null != idRanges) {
for (IdRange range : idRanges) {
if (range.includes(uid)) {
return true;
}
}
}
return false;
}
@Override
public String toString() {
return lowVal == highVal ?
Long.toString(lowVal)
: lowVal + ":" + highVal;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof IdRange)) return false;
IdRange idRange = (IdRange) o;
return lowVal == idRange.lowVal &&
highVal == idRange.highVal;
}
@Override
public int hashCode() {
return Objects.hash(lowVal, highVal);
}
}