/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * 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 net.java.sip.communicator.impl.filehistory; import static net.java.sip.communicator.service.history.HistoryService.*; import java.io.*; import java.text.*; import java.util.*; import net.java.sip.communicator.service.contactlist.*; import net.java.sip.communicator.service.filehistory.*; import net.java.sip.communicator.service.history.*; import net.java.sip.communicator.service.history.records.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.osgi.framework.*; /** * File History Service stores info for file transfers from various protocols. * Uses History Service. * * @author Damian Minkov */ public class FileHistoryServiceImpl implements FileHistoryService, ServiceListener, FileTransferStatusListener, FileTransferListener { /** * The logger for this class. */ private static final Logger logger = Logger.getLogger(FileHistoryServiceImpl.class); private static String[] STRUCTURE_NAMES = new String[] { "file", "dir", "date", "status", "id"}; private static final String FILE_TRANSFER_ACTIVE = "active"; private static HistoryRecordStructure recordStructure = new HistoryRecordStructure(STRUCTURE_NAMES); // the field used to search by keywords private static final String SEARCH_FIELD = "file"; /** * The BundleContext that we got from the OSGI bus. */ private BundleContext bundleContext = null; /** * The <tt>HistoryService</tt> reference. */ private HistoryService historyService = null; /** * Starts the service. Check the current registered protocol providers * which supports FileTransfer and adds a listener to them. * * @param bc BundleContext */ public void start(BundleContext bc) { if (logger.isDebugEnabled()) logger.debug("Starting the file history implementation."); this.bundleContext = bc; // start listening for newly register or removed protocol providers bc.addServiceListener(this); Collection<ServiceReference<ProtocolProviderService>> ppsRefs = ServiceUtils.getServiceReferences( bc, ProtocolProviderService.class); // in case we found any if (!ppsRefs.isEmpty()) { if (logger.isDebugEnabled()) { logger.debug( "Found " + ppsRefs.size() + " already installed providers."); } for (ServiceReference<ProtocolProviderService> ppsRef : ppsRefs) { ProtocolProviderService pps = bc.getService(ppsRef); handleProviderAdded(pps); } } } /** * Stops the service. * * @param bc BundleContext */ public void stop(BundleContext bc) { bc.removeServiceListener(this); Collection<ServiceReference<ProtocolProviderService>> ppsRefs = ServiceUtils.getServiceReferences( bc, ProtocolProviderService.class); // in case we found any if (!ppsRefs.isEmpty()) { for (ServiceReference<ProtocolProviderService> ppsRef : ppsRefs) { ProtocolProviderService pps = bc.getService(ppsRef); handleProviderRemoved(pps); } } } /** * Used to attach the File History Service to existing or * just registered protocol provider. Checks if the provider has implementation * of OperationSetFileTransfer * * @param provider ProtocolProviderService */ private void handleProviderAdded(ProtocolProviderService provider) { if (logger.isDebugEnabled()) logger.debug("Adding protocol provider " + provider.getProtocolName()); // check whether the provider has a file transfer operation set OperationSetFileTransfer opSetFileTransfer = provider.getOperationSet(OperationSetFileTransfer.class); if (opSetFileTransfer != null) { opSetFileTransfer.addFileTransferListener(this); } else { if (logger.isTraceEnabled()) logger.trace("Service did not have a file transfer op. set."); } } /** * Removes the specified provider from the list of currently known providers * * @param provider the ProtocolProviderService that has been unregistered. */ private void handleProviderRemoved(ProtocolProviderService provider) { OperationSetFileTransfer opSetFileTransfer = provider.getOperationSet(OperationSetFileTransfer.class); if (opSetFileTransfer != null) { opSetFileTransfer.addFileTransferListener(this); } } /** * Set the history service. * * @param historyService HistoryService */ public void setHistoryService(HistoryService historyService) { this.historyService = historyService; } /** * Gets all the history readers for the contacts in the given MetaContact * @param contact MetaContact * @return Hashtable */ private Map<Contact, HistoryReader> getHistoryReaders(MetaContact contact) { Map<Contact, HistoryReader> readers = new Hashtable<Contact, HistoryReader>(); Iterator<Contact> iter = contact.getContacts(); while (iter.hasNext()) { Contact item = iter.next(); try { History history = this.getHistory(null, item); readers.put(item, history.getReader()); } catch (IOException e) { logger.error("Could not read history", e); } } return readers; } private FileRecord createFileRecordFromHistoryRecord( HistoryRecord hr, Contact contact) { String file = null; String dir = null; Date date = new Date(0); String status = null; String id = null; SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); for (int i = 0; i < hr.getPropertyNames().length; i++) { String propName = hr.getPropertyNames()[i]; if (propName.equals(STRUCTURE_NAMES[0])) file = hr.getPropertyValues()[i]; else if (propName.equals(STRUCTURE_NAMES[1])) dir = hr.getPropertyValues()[i]; else if (propName.equals(STRUCTURE_NAMES[2])) { try { date = sdf.parse(hr.getPropertyValues()[i]); } catch (ParseException e) { date = new Date(Long.valueOf(hr.getPropertyValues()[i])); } } else if (propName.equals(STRUCTURE_NAMES[3])) status = hr.getPropertyValues()[i]; else if (propName.equals(STRUCTURE_NAMES[4])) id = hr.getPropertyValues()[i]; } return new FileRecord(id, contact, dir, date, new File(file), status); } /** * Returns all the file transfers made after the given date * * @param contact MetaContact the receiver or sender of the file * @param startDate Date the start date of the transfers * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByStartDate( MetaContact contact, Date startDate) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findByStartDate(startDate); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns all the file transfers made before the given date * * @param contact MetaContact the receiver or sender of the file * @param endDate Date the end date of the transfers * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByEndDate(MetaContact contact, Date endDate) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findByEndDate(endDate); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns all the file transfers made between the given dates and * having the given keywords in the filename * * @param contact MetaContact the receiver or sender of the file * @param startDate Date the start date of the transfers * @param endDate Date the end date of the transfers * @param keywords array of keywords * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByPeriod(MetaContact contact, Date startDate, Date endDate, String[] keywords) throws RuntimeException { return findByPeriod(contact, startDate, endDate, keywords, false); } /** * Returns all the file transfers made between the given dates * and having the given keywords in the filename * * @param contact MetaContact the receiver or sender of the file * @param startDate Date the start date of the transfers * @param endDate Date the end date of the transfers * @param keywords array of keywords * @param caseSensitive is keywords search case sensitive * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByPeriod(MetaContact contact, Date startDate, Date endDate, String[] keywords, boolean caseSensitive) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findByPeriod( startDate, endDate, keywords, SEARCH_FIELD, caseSensitive); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns all the file transfers made between the given dates * * @param contact MetaContact the receiver or sender of the file * @param startDate Date the start date of the transfers * @param endDate Date the end date of the transfers * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByPeriod( MetaContact contact, Date startDate, Date endDate) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findByPeriod(startDate, endDate); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns the supplied number of file transfers * * @param contact MetaContact the receiver or sender of the file * @param count filetransfer count * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findLast(MetaContact contact, int count) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findLast(count); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns all the file transfers having the given keyword in the filename * * @param contact MetaContact the receiver or sender of the file * @param keyword keyword * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByKeyword( MetaContact contact, String keyword) throws RuntimeException { return findByKeyword(contact, keyword, false); } /** * Returns all the file transfers having the given keyword in the filename * * @param contact MetaContact the receiver or sender of the file * @param keyword keyword * @param caseSensitive is keywords search case sensitive * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByKeyword( MetaContact contact, String keyword, boolean caseSensitive) throws RuntimeException { return findByKeywords(contact, new String[]{keyword}, caseSensitive); } /** * Returns all the file transfers having the given keywords in the filename * * @param contact MetaContact the receiver or sender of the file * @param keywords keyword * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByKeywords( MetaContact contact, String[] keywords) throws RuntimeException { return findByKeywords(contact, keywords, false); } /** * Returns all the file transfershaving the given keywords in the filename * * @param contact MetaContact the receiver or sender of the file * @param keywords keyword * @param caseSensitive is keywords search case sensitive * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findByKeywords( MetaContact contact, String[] keywords, boolean caseSensitive) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findByKeywords(keywords, SEARCH_FIELD, caseSensitive); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } return result; } /** * Returns the supplied number of recent file transfers after the given date * * @param contact MetaContact the receiver or sender of the file * @param date transfers after date * @param count transfers count * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findFirstRecordsAfter( MetaContact contact, Date date, int count) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findFirstRecordsAfter(date, count); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } LinkedList<FileRecord> resultAsList = new LinkedList<FileRecord>(result); int toIndex = count; if(toIndex > resultAsList.size()) toIndex = resultAsList.size(); return resultAsList.subList(0, toIndex); } /** * Returns the supplied number of recent file transfers before the given date * * @param contact MetaContact the receiver or sender of the file * @param date transfers before date * @param count transfers count * @return Collection of FileRecords * @throws RuntimeException */ public Collection<FileRecord> findLastRecordsBefore( MetaContact contact, Date date, int count) throws RuntimeException { TreeSet<FileRecord> result = new TreeSet<FileRecord>(new FileRecordComparator()); // get the readers for this contact Map<Contact, HistoryReader> readers = getHistoryReaders(contact); for (Map.Entry<Contact, HistoryReader> readerEntry : readers.entrySet()) { Contact c = readerEntry.getKey(); HistoryReader reader = readerEntry.getValue(); // add the progress listeners Iterator<HistoryRecord> recs = reader.findLastRecordsBefore(date, count); while (recs.hasNext()) { result.add(createFileRecordFromHistoryRecord(recs.next(), c)); } } LinkedList<FileRecord> resultAsList = new LinkedList<FileRecord>(result); int startIndex = resultAsList.size() - count; if(startIndex < 0) startIndex = 0; return resultAsList.subList(startIndex, resultAsList.size()); } /** * When new protocol provider is registered we check * does it supports FileTransfer and if so add a listener to it * * @param serviceEvent ServiceEvent */ public void serviceChanged(ServiceEvent serviceEvent) { Object sService = bundleContext.getService(serviceEvent.getServiceReference()); if (logger.isTraceEnabled()) logger.trace("Received a service event for: " + sService.getClass().getName()); // we don't care if the source service is not a protocol provider if (! (sService instanceof ProtocolProviderService)) { return; } if (logger.isDebugEnabled()) logger.debug("Service is a protocol provider."); if (serviceEvent.getType() == ServiceEvent.REGISTERED) { if (logger.isDebugEnabled()) logger.debug("Handling registration of a new Protocol Provider."); this.handleProviderAdded((ProtocolProviderService)sService); } else if (serviceEvent.getType() == ServiceEvent.UNREGISTERING) { this.handleProviderRemoved( (ProtocolProviderService) sService); } } /** * Returns the history by specified local and remote contact * if one of them is null the default is used * * @param localContact Contact * @param remoteContact Contact * @return History * @throws IOException */ private History getHistory(Contact localContact, Contact remoteContact) throws IOException { String localId = localContact == null ? "default" : localContact .getAddress(); String remoteId = remoteContact == null ? "default" : remoteContact .getAddress(); String account = "unkown"; if (remoteContact != null) account = remoteContact.getProtocolProvider().getAccountID(). getAccountUniqueID(); HistoryID historyId = HistoryID.createFromRawID( new String[] { "filehistory", localId, account, remoteId }); return this.historyService.createHistory(historyId, recordStructure); } /** * Listens for changes in file transfers. * @param event */ public void statusChanged(FileTransferStatusChangeEvent event) { try { FileTransfer ft = event.getFileTransfer(); String status = getStatus(ft.getStatus()); // ignore events we don't need if(status == null) return; History history = getHistory(null, ft.getContact()); HistoryWriter historyWriter = history.getWriter(); historyWriter.updateRecord( STRUCTURE_NAMES[4], ft.getID(), STRUCTURE_NAMES[3], status); } catch (IOException e) { logger.error("Could not update file transfer log to history", e); } } private static String getDirection(int direction) { switch(direction) { case FileTransfer.IN : return FileRecord.IN; case FileTransfer.OUT : return FileRecord.OUT; default: return null; } } /** * Maps only the statuses we are interested in, otherwise returns null. * @param status the status as receive from FileTransfer * @return the corresponding status of FileRecord. */ private static String getStatus(int status) { switch(status) { case FileTransferStatusChangeEvent.CANCELED : return FileRecord.CANCELED; case FileTransferStatusChangeEvent.COMPLETED : return FileRecord.COMPLETED; case FileTransferStatusChangeEvent.FAILED : return FileRecord.FAILED; case FileTransferStatusChangeEvent.REFUSED : return FileRecord.REFUSED; default: return null; } } /** * We ignore filetransfer requests. * @param event */ public void fileTransferRequestReceived(FileTransferRequestEvent event) { try { IncomingFileTransferRequest req = event.getRequest(); History history = getHistory(null, req.getSender()); HistoryWriter historyWriter = history.getWriter(); SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT); historyWriter.addRecord(new String[]{ req.getFileName(), getDirection(FileTransfer.IN), sdf.format(event.getTimestamp()), FILE_TRANSFER_ACTIVE, req.getID() }); } catch (IOException e) { logger.error("Could not add file transfer log to history", e); } } /** * New file transfer was created. * @param event fileTransfer */ public void fileTransferCreated(FileTransferCreatedEvent event) { FileTransfer fileTransfer = event.getFileTransfer(); fileTransfer.addStatusListener(this); try { History history = getHistory(null, fileTransfer.getContact()); HistoryWriter historyWriter = history.getWriter(); if (fileTransfer.getDirection() == FileTransfer.IN) { historyWriter.updateRecord( STRUCTURE_NAMES[4], fileTransfer.getID(), STRUCTURE_NAMES[0], fileTransfer.getLocalFile().getCanonicalPath()); } else if (fileTransfer.getDirection() == FileTransfer.OUT) { SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT); historyWriter.addRecord(new String[]{ fileTransfer.getLocalFile().getCanonicalPath(), getDirection(FileTransfer.OUT), sdf.format(event.getTimestamp()), FILE_TRANSFER_ACTIVE, fileTransfer.getID() }); } } catch (IOException e) { logger.error("Could not add file transfer log to history", e); } } /** * Called when a new <tt>IncomingFileTransferRequest</tt> has been rejected. * * @param event the <tt>FileTransferRequestEvent</tt> containing the * received request which was rejected. */ public void fileTransferRequestRejected(FileTransferRequestEvent event) { try { IncomingFileTransferRequest req = event.getRequest(); History history = getHistory(null, req.getSender()); HistoryWriter historyWriter = history.getWriter(); historyWriter.updateRecord( STRUCTURE_NAMES[4], req.getID(), STRUCTURE_NAMES[3], FileRecord.REFUSED ); } catch (IOException e) { logger.error("Could not add file transfer log to history", e); } } public void fileTransferRequestCanceled(FileTransferRequestEvent event) { try { IncomingFileTransferRequest req = event.getRequest(); History history = getHistory(null, req.getSender()); HistoryWriter historyWriter = history.getWriter(); historyWriter.updateRecord( STRUCTURE_NAMES[4], req.getID(), STRUCTURE_NAMES[3], FileRecord.CANCELED ); } catch (IOException e) { logger.error("Could not add file transfer log to history", e); } } /** * Permanently removes all locally stored file history. * * @throws java.io.IOException * Thrown if the history could not be removed due to a IO error. */ public void eraseLocallyStoredHistory() throws IOException { HistoryID historyId = HistoryID.createFromRawID( new String[] { "filehistory" }); historyService.purgeLocallyStoredHistory(historyId); } /** * Permanently removes locally stored file history for the metacontact. * * @throws java.io.IOException * Thrown if the history could not be removed due to a IO error. */ public void eraseLocallyStoredHistory(MetaContact contact) throws IOException { Iterator<Contact> iter = contact.getContacts(); while (iter.hasNext()) { Contact item = iter.next(); History history = this.getHistory(null, item); historyService.purgeLocallyStoredHistory(history.getID()); } } /** * Used to compare FileRecords * and to be ordered in TreeSet according their timestamp */ private static class FileRecordComparator implements Comparator<FileRecord> { public int compare(FileRecord o1, FileRecord o2) { Date date1 = o1.getDate(); Date date2 = o2.getDate(); return date1.compareTo(date2); } } }