package com.limegroup.gnutella.messages;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.limewire.core.settings.MessageSettings;
import org.limewire.core.settings.SearchSettings;
import org.limewire.io.BadGGEPPropertyException;
import org.limewire.io.GGEP;
import org.limewire.io.GUID;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.security.AddressSecurityToken;
import org.limewire.security.MACCalculatorRepositoryManager;
import org.limewire.service.ErrorService;
import org.limewire.util.ByteUtils;
import org.limewire.util.I18NConvert;
import org.limewire.util.StringUtils;
import org.xml.sax.SAXException;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UrnSet;
import com.limegroup.gnutella.util.QueryUtils;
import com.limegroup.gnutella.messages.HUGEExtension.GGEPBlock;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLDocumentFactory;
import com.limegroup.gnutella.xml.SchemaNotFoundException;
/**
* This class creates Gnutella query messages, either from scratch, or
* from data read from the network. Queries can contain query strings,
* XML query strings, URNs, etc. The minimum speed field is now used
* for bit flags to indicate such things as the firewalled status of
* the querier.<p>
*
* This class also has factory constructors for requeries originated
* from this LimeWire. These requeries have specially marked GUIDs
* that allow us to identify them as requeries.
*/
public class QueryRequestImpl extends AbstractMessage implements QueryRequest {
private static final Log LOG = LogFactory.getLog(QueryRequestImpl.class);
/**
* The payload for the query -- includes the query string, the
* XML query, any URNs, GGEP, etc.
*/
private final byte[] PAYLOAD;
/**
* The "min speed" field. This was originally used to specify
* a minimum speed for returned results, but it was never really
* used this way. As of LimeWire 3.0 (02/2003), the bits of
* this field were changed to specify things like the firewall
* status of the querier.
*/
private final int MIN_SPEED;
/**
* The query string.
*/
private final String QUERY;
/**
* The LimeXMLDocument of the rich query.
*/
private final LimeXMLDocument XML_DOC;
/**
* The feature that this query is.
*/
private int _featureSelector = 0;
private boolean _isSecurityTokenRequired;
/** If the query desires partial results */
private boolean _partialResultsDesired;
/**
* Whether or not the GGEP header for Do Not Proxy was found and its
* field is empty.
*/
private boolean _doNotProxy = false;
// HUGE v0.93 fields
/**
* Specific URNs requested.
*/
private final Set<URN> QUERY_URNS;
/**
* The Query Key associated with this query -- can be null.
*/
private final AddressSecurityToken QUERY_KEY;
/**
* The flag in the 'M' GGEP extension - if non-null, the query is requesting
* only certain types.
*/
private Integer _metaMask = null;
/**
* If we're re-originated this query for a leaf. This can be set/read
* after creation.
*/
private boolean originated = false;
/**
* Cached hash code for this instance.
*/
private volatile int _hashCode = 0;
/**
* Cached illegal characters in search strings.
*/
private static final char[] ILLEGAL_CHARS =
SearchSettings.ILLEGAL_CHARS.get();
/**
* Cache the maximum length for queries, in bytes.
*/
private static final int MAX_QUERY_LENGTH =
SearchSettings.MAX_QUERY_LENGTH.getValue();
/**
* Cache the maximum length for XML queries, in bytes.
*/
private static final int MAX_XML_QUERY_LENGTH =
SearchSettings.MAX_XML_QUERY_LENGTH.getValue();
/**
* Cache the max length for query string message field
*/
private static final int OLD_LW_MAX_QUERY_FIELD_LEN = 30;
/** Constructs a query. */
QueryRequestImpl(byte[] guid, byte ttl, int minSpeed,
String query, String richQuery,
Set<? extends URN> queryUrns,
AddressSecurityToken addressSecurityToken, boolean isFirewalled,
Network network, boolean canReceiveOutOfBandReplies,
int featureSelector, boolean doNotProxy,
int metaFlagMask, boolean normalize,
boolean canDoFWT,
LimeXMLDocumentFactory limeXMLDocumentFactory) {
// don't worry about getting the length right at first
super(guid, Message.F_QUERY, ttl, /* hops */ (byte)0, /* length */ 0,
network);
if(LOG.isDebugEnabled()) {
LOG.debug("Creating query request, OOB capable " +
canReceiveOutOfBandReplies + ", do not proxy " +
doNotProxy);
}
// make sure the query is normalized.
// (this may have been normalized elsewhere, but it's okay to do it again)
if(normalize && query != null)
query = I18NConvert.instance().getNorm(query);
if((query == null || query.length() == 0) &&
(richQuery == null || richQuery.length() == 0) &&
(queryUrns == null || queryUrns.size() == 0)) {
throw new IllegalArgumentException("cannot create empty query");
}
if(query != null && query.length() > MAX_QUERY_LENGTH) {
throw new IllegalArgumentException("query too big: " + query);
}
if(richQuery != null && richQuery.length() > MAX_XML_QUERY_LENGTH) {
throw new IllegalArgumentException("xml too big: " + richQuery);
}
if(query != null &&
!(queryUrns != null && queryUrns.size() > 0 &&
query.equals(QueryRequestImpl.DEFAULT_URN_QUERY))
&& hasIllegalChars(query)) {
throw new IllegalArgumentException("illegal chars: " + query);
}
if (featureSelector < 0)
throw new IllegalArgumentException("Bad feature = " +
featureSelector);
_featureSelector = featureSelector;
if ((metaFlagMask > 0) && (metaFlagMask < 4) || (metaFlagMask > 248))
throw new IllegalArgumentException("Bad Meta Flag = " +
metaFlagMask);
if (metaFlagMask > 0)
_metaMask = metaFlagMask;
// only set the minspeed if none was input...x
if (minSpeed == 0) {
// the new Min Speed format - looks reversed but
// it isn't because of ByteOrder.short2leb
minSpeed = SPECIAL_MINSPEED_MASK;
// set the firewall bit if i'm firewalled
if (isFirewalled && !isMulticast())
minSpeed |= SPECIAL_FIREWALL_MASK;
// if i'm firewalled and can do solicited, mark the query for fw
// transfer capability.
if (isFirewalled && canDoFWT)
minSpeed |= SPECIAL_FWTRANS_MASK;
// THE DEAL:
// if we can NOT receive out of band replies, we want in-band XML -
// so set the correct bit.
// if we can receive out of band replies, we do not want in-band XML
// we'll hope the out-of-band reply guys will provide us all
// necessary XML.
if(!canReceiveOutOfBandReplies) {
LOG.debug("Can't receive OOB replies, setting XML flag");
minSpeed |= SPECIAL_XML_MASK;
} else if(!SearchSettings.DISABLE_OOB_V2.getBoolean()) {
LOG.debug("Setting OOBv2 flag");
minSpeed |= SPECIAL_OUTOFBAND_MASK;
}
}
MIN_SPEED = minSpeed;
if (query == null) {
this.QUERY = "";
} else {
this.QUERY = query;
}
if (richQuery == null || richQuery.equals("")) {
this.XML_DOC = null;
} else {
LimeXMLDocument doc = null;
try {
doc = limeXMLDocumentFactory.createLimeXMLDocument(richQuery);
} catch (SAXException ignored) {
} catch (SchemaNotFoundException ignored) {
} catch (IOException ignored) {
}
this.XML_DOC = doc;
}
Set<URN> tempQueryUrns = null;
if (queryUrns != null) {
tempQueryUrns = new UrnSet(queryUrns);
} else {
tempQueryUrns = URN.NO_URN_SET;
}
this.QUERY_KEY = addressSecurityToken;
this._doNotProxy = doNotProxy;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ByteUtils.short2leb((short) MIN_SPEED, baos); // write minspeed
baos.write(getQueryFieldValue().getBytes("UTF-8")); // write query
baos.write(0); // null
// now write any & all HUGE v0.93 General Extension Mechanism
// extensions
// this specifies whether or not the extension was successfully
// written, meaning that the HUGE GEM delimiter should be
// written before the next extension
boolean addDelimiterBefore = false;
byte[] richQueryBytes = null;
if (XML_DOC != null) {
assert richQuery != null;
richQueryBytes = richQuery.getBytes("UTF-8");
}
// add the rich query
addDelimiterBefore = writeGemExtension(baos, addDelimiterBefore, richQueryBytes);
// add the urns
addDelimiterBefore = writeGemExtensions(baos, addDelimiterBefore,
tempQueryUrns == null ? null : tempQueryUrns.iterator());
// add the GGEP Extension, if necessary....
// *----------------------------
// construct the GGEP block
GGEP ggepBlock = new GGEP(true); // do COBS
// add the query key?
if (this.QUERY_KEY != null) {
// get query key in byte form....
ByteArrayOutputStream qkBytes = new ByteArrayOutputStream();
this.QUERY_KEY.write(qkBytes);
ggepBlock.put(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT, qkBytes.toByteArray());
}
// add the What Is header
if (_featureSelector > 0)
ggepBlock.put(GGEPKeys.GGEP_HEADER_FEATURE_QUERY, _featureSelector);
// add a GGEP-block if we shouldn't proxy
if (_doNotProxy) {
LOG.debug("Adding do not proxy OOB header");
ggepBlock.put(GGEPKeys.GGEP_HEADER_NO_PROXY);
}
// add a meta flag
if (_metaMask != null)
ggepBlock.put(GGEPKeys.GGEP_HEADER_META, _metaMask.intValue());
// mark oob query to require support of security tokens
if (canReceiveOutOfBandReplies) {
LOG.debug("Adding secure OOB header");
_isSecurityTokenRequired = true;
ggepBlock.put(GGEPKeys.GGEP_HEADER_SECURE_OOB);
}
if (SearchSettings.desiresPartialResults()) {
_partialResultsDesired = true;
ggepBlock.put(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_PREFIX);
}
if (QUERY.length() > OLD_LW_MAX_QUERY_FIELD_LEN) {
ggepBlock.put(GGEPKeys.GGEP_HEADER_EXTENDED_QUERY, QUERY);
}
// if there are GGEP headers, write them out...
if (!ggepBlock.isEmpty()) {
ByteArrayOutputStream ggepBytes = new ByteArrayOutputStream();
ggepBlock.write(ggepBytes);
// write out GGEP
addDelimiterBefore = writeGemExtension(baos, addDelimiterBefore,
ggepBytes.toByteArray());
}
// ----------------------------*
baos.write(0); // final null
}
catch(UnsupportedEncodingException uee) {
//this should never happen from the getBytes("UTF-8") call
//but there are UnsupportedEncodingExceptions being reported
//with UTF-8.
//Is there other information we want to pass in as the message?
throw new IllegalArgumentException("could not get UTF-8 bytes for query :"
+ QUERY
+ " with richquery :"
+ richQuery);
}
catch (IOException e) {
ErrorService.error(e);
}
PAYLOAD = baos.toByteArray();
updateLength(PAYLOAD.length);
this.QUERY_URNS = Collections.unmodifiableSet(tempQueryUrns);
}
/**
* Generate query string field based on query string ({@link #QUERY} value
* <p>
* Assumption:
* {@link #QUERY} is already set prior to this method getting called
*
* @return String representing the query field in the message
*/
private String getQueryFieldValue() {
// extract keywords from query
Set<String> keywords = QueryUtils.extractKeywords(QUERY, true);
// conditions for which the query field will be identical to the query string
if ((QUERY.length() <= OLD_LW_MAX_QUERY_FIELD_LEN) || (keywords.isEmpty())) {
return QUERY;
}
// adding keywords that fit when appended to query string field, skipping keywords that do not fit.
return QueryUtils.constructQueryStringFromKeywords(OLD_LW_MAX_QUERY_FIELD_LEN, keywords);
}
/**
* Build a new query with data snatched from network.
*
* @param guid the message guid
* @param ttl the time to live of the query
* @param hops the hops of the query
* @param payload the query payload, containing the query string and any
* extension strings
* @param network the network that this query came from.
* @throws <tt>BadPacketException</tt> if this is not a valid query
*/
QueryRequestImpl(byte[] guid, byte ttl, byte hops, byte[] payload, Network network,
LimeXMLDocumentFactory limeXMLDocumentFactory, MACCalculatorRepositoryManager manager)
throws BadPacketException {
super(guid, Message.F_QUERY, ttl, hops, payload.length, network);
PAYLOAD = payload;
QueryRequestPayloadParser parser = new QueryRequestPayloadParser(payload, manager);
QUERY = parser.query;
LimeXMLDocument tempDoc = null;
try {
tempDoc = limeXMLDocumentFactory.createLimeXMLDocument(parser.richQuery);
} catch (SAXException ignored) {
} catch (SchemaNotFoundException ignored) {
} catch(IOException ignored) {
}
this.XML_DOC = tempDoc;
MIN_SPEED = parser.minSpeed;
_featureSelector = parser.featureSelector;
_doNotProxy = parser.doNotProxy;
_metaMask = parser.metaMask;
_isSecurityTokenRequired = parser.hasSecurityTokenRequest;
_partialResultsDesired = parser.partialResultsDesired;
if (parser.queryUrns == null) {
QUERY_URNS = Collections.emptySet();
} else {
QUERY_URNS = Collections.unmodifiableSet(parser.queryUrns);
}
QUERY_KEY = parser.addressSecurityToken;
if (QUERY.length() == 0 && parser.richQuery.length() == 0 && QUERY_URNS.size() == 0) {
throw new BadPacketException("empty query");
}
if (QUERY.length() > MAX_QUERY_LENGTH) {
// throw BadPacketException.QUERY_TOO_BIG;
throw new BadPacketException("query too big: " + QUERY);
}
if(parser.richQuery.length() > MAX_XML_QUERY_LENGTH) {
//throw BadPacketException.XML_QUERY_TOO_BIG;
throw new BadPacketException("xml too big: " + parser.richQuery);
}
if(!(QUERY_URNS.size() > 0 && QUERY.equals(QueryRequestImpl.DEFAULT_URN_QUERY))
&& hasIllegalChars(QUERY)) {
//throw BadPacketException.ILLEGAL_CHAR_IN_QUERY;
throw new BadPacketException("illegal chars: " + QUERY);
}
}
private static boolean hasIllegalChars(String query) {
return StringUtils.containsCharacters(query,ILLEGAL_CHARS);
}
/**
* Returns a new GUID appropriate for query requests. If isRequery,
* the GUID query is marked.
*/
public static byte[] newQueryGUID(boolean isRequery) {
if (isRequery)
return GUID.makeGuidRequery();
byte [] ret = GUID.makeGuid();
if (MessageSettings.STAMP_QUERIES.getValue())
GUID.timeStampGuid(ret);
return ret;
}
@Override
protected void writePayload(OutputStream out) throws IOException {
out.write(PAYLOAD);
}
/**
* Accessor for the payload of the query hit.
*
* @return the query hit payload
*/
public byte[] getPayload() {
return PAYLOAD;
}
/**
* Returns the query string of this message.<p>
*
* The caller should not call the getBytes() method on the returned value,
* as this seems to cause problems on the Japanese Macintosh. If you need
* the raw bytes of the query string, call getQueryByteAt(int).
*/
public String getQuery() {
return QUERY;
}
/**
* Returns the rich query LimeXMLDocument.
*
* @return the rich query LimeXMLDocument
*/
public LimeXMLDocument getRichQuery() {
return XML_DOC;
}
/**
* Helper method used internally for getting the rich query string.
*/
public String getRichQueryString() {
if( XML_DOC == null )
return null;
else
return XML_DOC.getXMLString();
}
/**
* Returns the <tt>Set</tt> of <tt>URN</tt> instances for this query.
*
* @return the <tt>Set</tt> of <tt>URN</tt> instances for this query, which
* may be empty (not null) if no URNs were requested
*/
public Set<URN> getQueryUrns() {
return QUERY_URNS;
}
/**
* Returns whether or not this query contains URNs.
*
* @return <tt>true</tt> if this query contains URNs,<tt>false</tt>
* otherwise
*/
public boolean hasQueryUrns() {
return !QUERY_URNS.isEmpty();
}
/**
* Note: the minimum speed can be represented as a 2-byte unsigned number,
* but Java shorts are signed. Hence we must use an int. The value returned
* is always smaller than 2^16.
*/
public int getMinSpeed() {
return MIN_SPEED;
}
/**
* Returns true if the query source is a firewalled servent.
*/
public boolean isFirewalledSource() {
if ( !isMulticast() ) {
if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
if ((MIN_SPEED & SPECIAL_FIREWALL_MASK) > 0)
return true;
}
}
return false;
}
/**
* Returns true if the query source desires Lime meta-data in responses.
*/
public boolean desiresXMLResponses() {
if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
if ((MIN_SPEED & SPECIAL_XML_MASK) > 0)
return true;
}
return false;
}
/**
* Returns true if the query source can do a firewalled transfer.
*/
public boolean canDoFirewalledTransfer() {
if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
if ((MIN_SPEED & SPECIAL_FWTRANS_MASK) > 0)
return true;
}
return false;
}
/**
* Returns true if the query source can accept out-of-band replies for
* any supported protocol version.
* <p>
* Use getReplyAddress() and getReplyPort() if this is true to know where to
* it. Always send XML if you are sending an out-of-band reply.
*/
public boolean desiresOutOfBandReplies() {
return desiresOutOfBandRepliesV2() || desiresOutOfBandRepliesV3();
}
/**
* Returns true if sender desires out-of-band replies for protocol version
* 2.
*/
public boolean desiresOutOfBandRepliesV2() {
if ((MIN_SPEED & SPECIAL_MINSPEED_MASK) > 0) {
if ((MIN_SPEED & SPECIAL_OUTOFBAND_MASK) > 0)
return true;
}
return false;
}
/**
* Returns true if sender desires out-of-band replies for protocol version
* 3.
*/
public boolean desiresOutOfBandRepliesV3() {
return isSecurityTokenRequired();
}
/**
* Returns true if the query source does not want you to proxy for it.
*/
public boolean doNotProxy() {
return _doNotProxy;
}
/**
* Returns true if this query is for 'What is new?' content, i.e. usually
* the top 3 YOUNGEST files in your library.
*/
public boolean isWhatIsNewRequest() {
return _featureSelector == FeatureSearchData.WHAT_IS_NEW;
}
/**
* Returns true if this is a feature query.
*/
public boolean isFeatureQuery() {
return _featureSelector > 0;
}
/**
* @return whether this is a browse host query
*/
public boolean isBrowseHostQuery() {
return INDEXING_QUERY.equals(getQuery());
}
/**
* Returns 0 if this is not a "feature" query, else it returns the selector
* of the feature query, e.g. What Is New returns 1.
*/
public int getFeatureSelector() {
return _featureSelector;
}
/**
* Returns true if the query request has a security token request,
* this implies the sender requests OOB replies, protocol version 3.
*/
public boolean isSecurityTokenRequired() {
return _isSecurityTokenRequired;
}
public boolean desiresPartialResults() {
return _partialResultsDesired;
}
/** Returns the address to send a out-of-band reply to. Only useful
* when desiresOutOfBandReplies() == true.
*/
public String getReplyAddress() {
return (new GUID(getGUID())).getIP();
}
/** Returns true if the input bytes match the OOB address of this query.
*/
public boolean matchesReplyAddress(byte[] ip) {
return (new GUID(getGUID())).matchesIP(ip);
}
/** Returns the port to send a out-of-band reply to. Only useful
* when desiresOutOfBandReplies() == true.
*/
public int getReplyPort() {
return (new GUID(getGUID())).getPort();
}
/**
* Accessor for whether or not this is a requery from a LimeWire.
*
* @return <tt>true</tt> if it is an automated requery from a LimeWire,
* otherwise <tt>false</tt>
*/
public boolean isLimeRequery() {
return GUID.isLimeRequeryGUID(getGUID());
}
/**
* @return true if this is likely a query for LimeWire.
*/
public boolean isQueryForLW() {
for (String term : SearchSettings.LIME_SEARCH_TERMS.get()) {
if (getQuery().length() > 0 &&
getQuery().toLowerCase(Locale.US).contains(term))
return true;
if (getRichQuery() != null) {
for (String keyword : getRichQuery().getKeyWords())
if (keyword.toLowerCase(Locale.US).contains(term))
return true;
}
}
return false;
}
/**
* Returns the AddressSecurityToken associated with this Request. May very well be
* null. Usually only UDP QueryRequests will have non-null QueryKeys.
*/
public AddressSecurityToken getQueryKey() {
return QUERY_KEY;
}
/** @return true if the query has no constraints on the type of results
* it wants back.
*/
public boolean desiresAll() {
return (_metaMask == null);
}
/** @return true if the query desires 'Audio' results back.
*/
public boolean desiresAudio() {
if (_metaMask != null)
return ((_metaMask & AUDIO_MASK) > 0);
return true;
}
/** @return true if the query desires 'Video' results back.
*/
public boolean desiresVideo() {
if (_metaMask != null)
return ((_metaMask & VIDEO_MASK) > 0);
return true;
}
/** @return true if the query desires 'Document' results back.
*/
public boolean desiresDocuments() {
if (_metaMask != null)
return ((_metaMask & DOC_MASK) > 0);
return true;
}
/** @return true if the query desires 'Image' results back.
*/
public boolean desiresImages() {
if (_metaMask != null)
return ((_metaMask & IMAGE_MASK) > 0);
return true;
}
/** @return true if the query desires 'Programs/Packages' for Windows
* results back.
*/
public boolean desiresWindowsPrograms() {
if (_metaMask != null)
return ((_metaMask & WIN_PROG_MASK) > 0);
return true;
}
/** @return true if the query desires 'Programs/Packages' for Linux/OSX
* results back.
*/
public boolean desiresLinuxOSXPrograms() {
if (_metaMask != null)
return ((_metaMask & LIN_PROG_MASK) > 0);
return true;
}
/**
* Returns the mask of allowed programs.
*/
public int getMetaMask() {
if (_metaMask != null)
return _metaMask;
return 0;
}
/** Marks this as being an re-originated query. */
public void originate() {
originated = true;
}
/** Determines if this is an originated query. */
public boolean isOriginated() {
return originated;
}
public boolean shouldIncludeXMLInResponse() {
return desiresXMLResponses() || desiresOutOfBandReplies();
}
@Override
public Class<? extends Message> getHandlerClass() {
return QueryRequest.class;
}
/**
* @effects Writes given extension string to given stream, adding
* delimiter if necessary, reporting whether next call should add
* delimiter. ext may be null or zero-length, in which case this is noop.
*/
protected boolean writeGemExtension(OutputStream os,
boolean addPrefixDelimiter,
byte[] extBytes) throws IOException {
if (extBytes == null || (extBytes.length == 0)) {
return addPrefixDelimiter;
}
if(addPrefixDelimiter) {
os.write(0x1c);
}
os.write(extBytes);
return true; // any subsequent extensions should have delimiter
}
/**
* @effects Writes each extension string in exts to given stream,
* adding delimiters as necessary. exts may be null or empty, in
* which case this is noop.
*/
protected boolean writeGemExtensions(OutputStream os,
boolean addPrefixDelimiter,
Iterator<URN> iter) throws IOException {
while(iter.hasNext()) {
addPrefixDelimiter = writeGemExtension(os, addPrefixDelimiter,
StringUtils.toAsciiBytes(iter.next().toString()));
}
return addPrefixDelimiter; // will be true is anything at all was written
}
/**
* @effects utility function to read null-terminated byte[] from stream
*/
protected static byte[] readNullTerminatedBytes(InputStream is)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((is.available()>0)&&(i=is.read())!=0) {
baos.write(i);
}
return baos.toByteArray();
}
@Override
public int hashCode() {
if (_hashCode == 0) {
int result = 17;
result = (37 * result) + QUERY.hashCode();
if (XML_DOC != null)
result = (37 * result) + XML_DOC.hashCode();
result = (37 * result) + QUERY_URNS.hashCode();
if (QUERY_KEY != null) {
result = (37 * result) + QUERY_KEY.hashCode();
}
// TODO:: ADD GUID!!
_hashCode = result;
}
return _hashCode;
}
// overrides Object.toString
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof QueryRequestImpl))
return false;
QueryRequestImpl qr = (QueryRequestImpl) o;
return (MIN_SPEED == qr.MIN_SPEED && QUERY.equals(qr.QUERY)
&& (XML_DOC == null ? qr.XML_DOC == null : XML_DOC.equals(qr.XML_DOC))
&& QUERY_URNS.equals(qr.QUERY_URNS) && Arrays.equals(getGUID(), qr.getGUID()) && Arrays
.equals(PAYLOAD, qr.PAYLOAD));
}
@Override
public String toString() {
return "<query: \"" + getQuery() + "\", " + "ttl: " + getTTL() + ", " + "hops: "
+ getHops() + ", " + "meta: \"" + getRichQueryString() + "\", " + "urns: "
+ getQueryUrns().size() + ">";
}
static byte[] patchInGGEP(byte[] payload, GGEP ggep, MACCalculatorRepositoryManager manager)
throws BadPacketException {
QueryRequestPayloadParser parser = new QueryRequestPayloadParser(payload, manager);
HUGEExtension huge = parser.huge;
if (huge != null) {
// we write in the last modifiable block if available, so our
// values are still there in the merged version that is read back
// from the network: this is not good
GGEPBlock block = getLastBlock(huge.getGGEPBlocks());
if (block != null) {
GGEP merge = new GGEP(true);
// first merge in original block and then ours, to make sure
// our values prevail
merge.merge(block.getGGEP());
merge.merge(ggep);
return insertBytes(payload, parser.hugeStart + block.getStartPos(), parser.hugeStart + block.getEndPos(), merge.toByteArray());
}
}
if (isFirstNullByteAfterOffset(payload, payload.length - 1, 2)) {
// if ggep is appended after query string keep 0 delimiter
return insertGGEP(payload, payload.length, payload.length, ggep.toByteArray(), true);
}
else if (payload[payload.length - 1] != 0x1C) {
return insertGGEP(payload, payload.length - 1, payload.length - 1, ggep.toByteArray(), true);
}
else {
return insertGGEP(payload, payload.length, payload.length, ggep.toByteArray(), false);
}
}
/**
* Returns true if byte at <code>index</code> is 0 and it's the first
* one in the payload signifying the end of the query string.
*/
private static boolean isFirstNullByteAfterOffset(byte[] payload, int index, int offset) {
if (payload[index] != 0x00) {
return false;
}
for (int i = offset; i < index; i++) {
if (payload[i] == 0x00) {
return false;
}
}
return true;
}
/**
* Return the last GGEPBlock in the list or null if there
* is none or if the list is empty.
*/
private static GGEPBlock getLastBlock(List<GGEPBlock> blocks) {
return blocks.isEmpty() ? null : blocks.get(blocks.size() - 1);
}
private static byte[] insertGGEP(byte[] payload, int start, int end, byte[] ggepBytes, boolean prependDelimiter) {
if (prependDelimiter) {
byte[] ggepBlock = new byte[ggepBytes.length + 1];
// set HUGE delimiter
ggepBlock[0] = 0x1C;
System.arraycopy(ggepBytes, 0, ggepBlock, 1, ggepBytes.length);
return insertBytes(payload, start, end, ggepBlock);
}
else {
return insertBytes(payload, start, end, ggepBytes);
}
}
private static byte[] insertBytes(byte[] payload, int start, int end, byte[] ggepBytes) {
byte[] newPayload = new byte[payload.length + ggepBytes.length - (end - start)];
System.arraycopy(payload, 0, newPayload, 0, start);
System.arraycopy(ggepBytes, 0, newPayload, start, ggepBytes.length);
if (end < payload.length) {
System.arraycopy(payload, end, newPayload, start + ggepBytes.length, payload.length - end);
}
return newPayload;
}
static class QueryRequestPayloadParser {
String query = "";
String richQuery = "";
int minSpeed = 0;
Set<URN> queryUrns = null;
Set<URN.Type> requestedUrnTypes = null;
AddressSecurityToken addressSecurityToken = null;
HUGEExtension huge;
int featureSelector;
boolean doNotProxy;
boolean doNotProxyV3;
Integer metaMask;
boolean hasSecurityTokenRequest;
boolean partialResultsDesired;
int hugeStart;
int hugeEnd;
public QueryRequestPayloadParser(byte[] payload, MACCalculatorRepositoryManager manager) throws BadPacketException {
try {
PositionByteArrayInputStream bais = new PositionByteArrayInputStream(payload);
short sp = ByteUtils.leb2short(bais);
minSpeed = ByteUtils.ushort2int(sp);
query = new String(readNullTerminatedBytes(bais), "UTF-8");
// handle extensions, which include rich query and URN stuff
hugeStart = bais.getPos();
byte[] extsBytes = readNullTerminatedBytes(bais);
huge = new HUGEExtension(extsBytes);
hugeEnd = bais.getPos();
GGEP ggep = huge.getGGEP();
if(ggep != null) {
try {
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT)) {
byte[] qkBytes = ggep.getBytes(GGEPKeys.GGEP_HEADER_QUERY_KEY_SUPPORT);
addressSecurityToken = new AddressSecurityToken(qkBytes, manager);
}
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_FEATURE_QUERY))
featureSelector = ggep.getInt(GGEPKeys.GGEP_HEADER_FEATURE_QUERY);
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_NO_PROXY)) {
doNotProxy = true;
}
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_META)) {
metaMask = ggep.getInt(GGEPKeys.GGEP_HEADER_META);
// if the value is something we can't handle, don't even set it
if ((metaMask < 4) || (metaMask > 248))
metaMask = null;
}
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_SECURE_OOB)) {
hasSecurityTokenRequest = true;
}
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_PREFIX))
partialResultsDesired = true;
if (ggep.hasKey(GGEPKeys.GGEP_HEADER_EXTENDED_QUERY)) {
this.query = ggep.getString(GGEPKeys.GGEP_HEADER_EXTENDED_QUERY);
}
} catch (BadGGEPPropertyException ignored) {}
}
queryUrns = huge.getURNS();
requestedUrnTypes = huge.getURNTypes();
for(String currMiscBlock : huge.getMiscBlocks()) {
if(!richQuery.equals(""))
break;
if (currMiscBlock.startsWith("<?xml"))
richQuery = currMiscBlock;
}
} catch(UnsupportedEncodingException uee) {
//couldn't build query from network due to unsupportedencodingexception
//so throw a BadPacketException
throw new BadPacketException(uee.getMessage());
} catch (IOException ioe) {
ErrorService.error(ioe);
}
}
static class PositionByteArrayInputStream extends ByteArrayInputStream {
public PositionByteArrayInputStream(byte[] buf) {
super(buf);
}
public int getPos() {
return pos;
}
}
}
}