package com.limegroup.gnutella.xml; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.Collection; import java.util.Collections; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.CreationTimeCache; import com.limegroup.gnutella.FileDesc; import com.limegroup.gnutella.FileManager; import com.limegroup.gnutella.FileManagerEvent; import com.limegroup.gnutella.FileEventListener; import com.limegroup.gnutella.Response; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.metadata.AudioMetaData; import com.limegroup.gnutella.metadata.MetaDataReader; import com.limegroup.gnutella.util.NameValue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class handles querying shared files with XML data and returning XML data * in replies. */ public class MetaFileManager extends FileManager { private static final Log LOG = LogFactory.getLog(MetaFileManager.class); private Saver saver; /** * Overrides FileManager.query. * * Used to search XML information in addition to normal searches. */ public synchronized Response[] query(QueryRequest request) { Response[] result = super.query(request); if (shouldIncludeXMLInResponse(request)) { LimeXMLDocument doc = request.getRichQuery(); if (doc != null) { Response[] metas = query(doc); if (metas != null) // valid query & responses. result = union(result, metas, doc); } } return result; } /** * Determines if this file has a valid XML match. */ protected boolean isValidXMLMatch(Response r, LimeXMLDocument doc) { return LimeXMLUtils.match(r.getDocument(), doc, true); } /** * Returns whether or not a response to this query should include XML. * Currently only includes XML if the request desires it or * if the request wants an out of band reply. */ protected boolean shouldIncludeXMLInResponse(QueryRequest qr) { return qr.desiresXMLResponses() || qr.desiresOutOfBandReplies(); } /** * Adds XML to the response. This assumes that shouldIncludeXMLInResponse * was already consulted and returned true. * * If the FileDesc has no XML documents, this does nothing. * If the FileDesc has one XML document, this sets it as the response doc. * If the FileDesc has multiple XML documents, this does nothing. * The reasoning behind not setting the document when there are multiple * XML docs is that presumably the query will be a 'rich' query, * and we want to include only the schema that was in the query. * * @param response the <tt>Response</tt> instance that XML should be * added to * @param fd the <tt>FileDesc</tt> that provides access to the * <tt>LimeXMLDocuments</tt> to add to the response */ protected void addXMLToResponse(Response response, FileDesc fd) { List docs = fd.getLimeXMLDocuments(); if( docs.size() == 0 ) return; if( docs.size() == 1 ) response.setDocument((LimeXMLDocument)docs.get(0)); } /** * Notification that a file has changed. * This implementation is different than FileManager's * in that it maintains the XML. * * Important note: This method is called AFTER the file has * changed. It is possible that the metadata we wanted to write * did not get written out completely. We should NOT attempt * to add the old metadata again, because we may end up * recursing infinitely trying to write this metadata. * However, it isn't very robust to blindly assume that the only * metadata associated with this file was audio metadata. * So, we make use of the fact that loadFile will only * add one type of metadata per file. We read the document tags off * the file and insert it first into the list, ensuring * that the existing metadata is the one that's added, short-circuiting * any infinite loops. */ public void fileChanged(File f) { if(LOG.isTraceEnabled()) LOG.debug("File Changed: " + f); FileDesc fd = getFileDescForFile(f); if( fd == null ) return; // store the creation time for later re-input CreationTimeCache ctCache = CreationTimeCache.instance(); final Long cTime = ctCache.getCreationTime(fd.getSHA1Urn()); List xmlDocs = fd.getLimeXMLDocuments(); if(LimeXMLUtils.isEditableFormat(f)) { try { LimeXMLDocument diskDoc = MetaDataReader.readDocument(f); xmlDocs = resolveWriteableDocs(xmlDocs, diskDoc); } catch(IOException e) { // if we were unable to read this document, // then simply add the file without metadata. xmlDocs = Collections.EMPTY_LIST; } } final FileDesc removed = removeFileIfShared(f, false); if(fd != removed) Assert.that(false, "wanted to remove: " + fd + "\ndid remove: " + removed); synchronized(this) { _needRebuild = true; } addFileIfShared(f, xmlDocs, false, _revision, new FileEventListener() { public void handleFileEvent(FileManagerEvent evt) { // Retarget the event for the GUI. FileManagerEvent newEvt = null; if(evt.isAddEvent()) { FileDesc fd = evt.getFileDescs()[0]; CreationTimeCache ctCache = CreationTimeCache.instance(); //re-populate the ctCache synchronized (ctCache) { ctCache.removeTime(fd.getSHA1Urn());//addFile() put lastModified ctCache.addTime(fd.getSHA1Urn(), cTime.longValue()); ctCache.commitTime(fd.getSHA1Urn()); } newEvt = new FileManagerEvent(MetaFileManager.this, FileManagerEvent.CHANGE, new FileDesc[]{removed,fd}); } else { newEvt = new FileManagerEvent(MetaFileManager.this, FileManagerEvent.REMOVE, removed); } dispatchFileEvent(newEvt); } }); } /** * Finds the audio metadata document in allDocs, and makes it's id3 fields * identical with the fields of id3doc (which are only id3). */ private List resolveWriteableDocs(List allDocs, LimeXMLDocument id3Doc) { LimeXMLDocument audioDoc = null; LimeXMLSchema audioSchema = LimeXMLSchemaRepository.instance().getSchema(AudioMetaData.schemaURI); for(Iterator iter = allDocs.iterator(); iter.hasNext() ;) { LimeXMLDocument doc = (LimeXMLDocument)iter.next(); if(doc.getSchema() == audioSchema) { audioDoc = doc; break; } } if(id3Doc.equals(audioDoc)) //No issue -- both documents are the same return allDocs; //did not modify list, keep using it List retList = new ArrayList(); retList.addAll(allDocs); if(audioDoc == null) {//nothing to resolve retList.add(id3Doc); return retList; } //OK. audioDoc exists, remove it retList.remove(audioDoc); //now add the non-id3 tags from audioDoc to id3doc List audioList = audioDoc.getOrderedNameValueList(); List id3List = id3Doc.getOrderedNameValueList(); for(int i = 0; i < audioList.size(); i++) { NameValue nameVal = (NameValue)audioList.get(i); if(AudioMetaData.isNonLimeAudioField(nameVal.getName())) id3List.add(nameVal); } audioDoc = new LimeXMLDocument(id3List, AudioMetaData.schemaURI); retList.add(audioDoc); return retList; } /** * Removes the LimeXMLDocuments associated with the removed * FileDesc from the various LimeXMLReplyCollections. */ protected synchronized FileDesc removeFileIfShared(File f, boolean notify) { FileDesc fd = super.removeFileIfShared(f, notify); // nothing removed, ignore. if( fd == null ) return null; SchemaReplyCollectionMapper mapper = SchemaReplyCollectionMapper.instance(); //Get the schema URI of each document and remove from the collection // We must remember the schemas and then remove the doc, or we will // get a concurrent mod exception because removing the doc also // removes it from the FileDesc. List xmlDocs = fd.getLimeXMLDocuments(); List schemas = new LinkedList(); for(Iterator i = xmlDocs.iterator(); i.hasNext(); ) schemas.add( ((LimeXMLDocument)i.next()).getSchemaURI() ); for(Iterator i = schemas.iterator(); i.hasNext(); ) { String uri = (String)i.next(); LimeXMLReplyCollection col = mapper.getReplyCollection(uri); if( col != null ) col.removeDoc( fd ); } _needRebuild = true; return fd; } /** * Notification that FileManager loading is starting. */ protected void loadStarted(int revision) { RouterService.getCallback().setAnnotateEnabled(false); // Load up new ReplyCollections. LimeXMLSchemaRepository schemaRepository = LimeXMLSchemaRepository.instance(); String[] schemas = schemaRepository.getAvailableSchemaURIs(); SchemaReplyCollectionMapper mapper = SchemaReplyCollectionMapper.instance(); for(int i = 0; i < schemas.length; i++) mapper.add(schemas[i], new LimeXMLReplyCollection(schemas[i])); super.loadStarted(revision); } /** * Notification that FileManager loading is finished. */ protected void loadFinished(int revision) { // save ourselves to disk every minute if (saver == null) { saver = new Saver(); RouterService.schedule(saver,60*1000,60*1000); } Collection replies = SchemaReplyCollectionMapper.instance().getCollections(); for(Iterator i = replies.iterator(); i.hasNext(); ) ((LimeXMLReplyCollection)i.next()).loadFinished(); RouterService.getCallback().setAnnotateEnabled(true); super.loadFinished(revision); } /** * Notification that a single FileDesc has its URNs. */ protected void loadFile(FileDesc fd, File file, List metadata, Set urns) { super.loadFile(fd, file, metadata, urns); boolean added = false; Collection replies = SchemaReplyCollectionMapper.instance().getCollections(); for(Iterator i = replies.iterator(); i.hasNext(); ) added |= (((LimeXMLReplyCollection)i.next()).initialize(fd, metadata) != null); for(Iterator i = replies.iterator(); i.hasNext(); ) added |= (((LimeXMLReplyCollection)i.next()).createIfNecessary(fd) != null); if(added) { synchronized(this) { _needRebuild = true; } } } protected void save() { if(isLoadFinished()) { Collection replies = SchemaReplyCollectionMapper.instance().getCollections(); for(Iterator i = replies.iterator(); i.hasNext(); ) ((LimeXMLReplyCollection)i.next()).writeMapToDisk(); } super.save(); } /** * Creates a new array, the size of which is less than or equal * to normals.length + metas.length. */ private Response[] union(Response[] normals, Response[] metas, LimeXMLDocument requested) { if(normals == null || normals.length == 0) return metas; if(metas == null || metas.length == 0) return normals; // It is important to use a HashSet here so that duplicate // responses are not sent. // Unfortunately, it is still possible that one Response // did not have metadata but the other did, causing two // responses for the same file. Set unionSet = new HashSet(); for(int i = 0; i < metas.length; i++) unionSet.add(metas[i]); for(int i = 0; i < normals.length; i++) unionSet.add(normals[i]); //The set contains all the elements that are the union of the 2 arrays Response[] retArray = new Response[unionSet.size()]; retArray = (Response[])unionSet.toArray(retArray); return retArray; } /** * build the QRT table * call to super.buildQRT and add XML specific Strings * to QRT */ protected void buildQRT() { super.buildQRT(); Iterator iter = getXMLKeyWords().iterator(); while(iter.hasNext()) _queryRouteTable.add((String)iter.next()); iter = getXMLIndivisibleKeyWords().iterator(); while(iter.hasNext()) _queryRouteTable.addIndivisible((String)iter.next()); } /** * Returns a list of all the words in the annotations - leaves out * numbers. The list also includes the set of words that is contained * in the names of the files. */ private List getXMLKeyWords(){ ArrayList words = new ArrayList(); //Now get a list of keywords from each of the ReplyCollections SchemaReplyCollectionMapper map=SchemaReplyCollectionMapper.instance(); LimeXMLSchemaRepository rep = LimeXMLSchemaRepository.instance(); String[] schemas = rep.getAvailableSchemaURIs(); LimeXMLReplyCollection collection; int len = schemas.length; for(int i=0;i<len;i++){ collection = map.getReplyCollection(schemas[i]); if(collection==null)//not loaded? skip it and keep goin' continue; words.addAll(collection.getKeyWords()); } return words; } /** @return A List of KeyWords from the FS that one does NOT want broken * upon hashing into a QRT. Initially being used for schema uri hashing. */ private List getXMLIndivisibleKeyWords() { ArrayList words = new ArrayList(); SchemaReplyCollectionMapper map=SchemaReplyCollectionMapper.instance(); LimeXMLSchemaRepository rep = LimeXMLSchemaRepository.instance(); String[] schemas = rep.getAvailableSchemaURIs(); LimeXMLReplyCollection collection; for (int i = 0; i < schemas.length; i++) { if (schemas[i] != null) words.add(schemas[i]); collection = map.getReplyCollection(schemas[i]); if(collection==null)//not loaded? skip it and keep goin' continue; words.addAll(collection.getKeyWordsIndivisible()); } return words; } /** * Returns an array of Responses that correspond to documents * that have a match given query document. */ private Response[] query(LimeXMLDocument queryDoc) { String schema = queryDoc.getSchemaURI(); SchemaReplyCollectionMapper mapper = SchemaReplyCollectionMapper.instance(); LimeXMLReplyCollection replyCol = mapper.getReplyCollection(schema); if(replyCol == null)//no matching reply collection for schema return null; List matchingReplies = replyCol.getMatchingReplies(queryDoc); //matchingReplies = a List of LimeXMLDocuments that match the query int s = matchingReplies.size(); if( s == 0 ) // no matching replies. return null; Response[] retResponses = new Response[s]; int z = 0; for(Iterator i = matchingReplies.iterator(); i.hasNext(); ) { LimeXMLDocument currDoc = (LimeXMLDocument)i.next(); File file = currDoc.getIdentifier();//returns null if none Response res = null; if (file == null) { //pure metadata (no file) res = new Response(LimeXMLProperties.DEFAULT_NONFILE_INDEX, 0, " "); } else { //meta-data about a specific file FileDesc fd = RouterService.getFileManager().getFileDescForFile(file); if( fd == null) { // if fd is null, MetaFileManager is out of synch with // FileManager -- this is bad. continue; } else { //we found a file with the right name res = new Response(fd); fd.incrementHitCount(); RouterService.getCallback().handleSharedFileUpdate(fd.getFile()); } } // Note that if any response was invalid, // the array will be too small, and we'll // have to resize it. res.setDocument(currDoc); retResponses[z] = res; z++; } if( z == 0 ) return null; // no responses // need to ensure that no nulls are returned in my response[] // z is a count of responses constructed, see just above... // s == retResponses.length if (z < s) { Response[] temp = new Response[z]; System.arraycopy(retResponses, 0, temp, 0, z); retResponses = temp; } return retResponses; } private class Saver implements Runnable { public void run() { if (!shutdown && isLoadFinished()) save(); } } }