/* * 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.plugin.thunderbird; import java.io.*; import java.util.*; import java.util.Map.Entry; import java.util.regex.*; import org.jitsi.util.StringUtils; import mork.*; import net.java.sip.communicator.service.contactsource.*; import net.java.sip.communicator.service.contactsource.ContactDetail.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; /** * Queries a Thunderbird address book for contacts matching the given pattern. * * @author Ingo Bauersachs */ public class ThunderbirdContactQuery extends AsyncContactQuery<ThunderbirdContactSourceService> { /** Class logger */ private final static Logger logger = Logger .getLogger(ThunderbirdContactQuery.class); /** * Creates a new instance of this class. * * @param owner The contact source that created this query. * @param query The pattern to match against the contacts database. */ public ThunderbirdContactQuery(ThunderbirdContactSourceService owner, Pattern query) { super(owner, query); } /** * Starts the query against the address book database. */ @Override protected void run() { String filename = super.getContactSource().getFilename(); File file = new File(filename); try { if (file.lastModified() > getContactSource().lastDatabaseFileChange) { // parse the Thunderbird Mork database InputStreamReader sr = new InputStreamReader(new FileInputStream(filename)); MorkDocument md = new MorkDocument(sr); sr.close(); // We now have rows in their tables and additional rows at // transaction level. Put the to a better format: // DB -> Tables -> Rows Map<String, Map<String, Row>> db = new HashMap<String, Map<String, Row>>(); for (Table t : md.getTables()) { String tableId = t.getTableId() + "/" + t.getScopeName(); Map<String, Row> table = db.get(tableId); if (table == null) { table = new HashMap<String, Row>(); db.put(tableId, table); } for (Row r : t.getRows()) { String scope = r.getScopeName(); if (scope == null) { scope = t.getScopeName(); } table.put(r.getRowId() + "/" + scope, r); } } // The additional rows at the root-level update/replace the ones // in the tables. There's usually neither a table nor a scope // defined, so lets just use the default. String defaultScope = md.getDicts().get(0).dereference("^80"); for (Row r : md.getRows()) { String scope = r.getScopeName(); if (scope == null) { scope = defaultScope; } String tableId = "1/" + scope; Map<String, Row> table = db.get(tableId); if (table == null) { table = new HashMap<String, Row>(); db.put(tableId, table); } String rowId = r.getRowId() + "/" + scope; if (rowId.startsWith("-")) { rowId = rowId.substring(1); } table.put(rowId, r); } super.getContactSource().database = db; super.getContactSource().defaultScope = defaultScope; super.getContactSource().lastDatabaseFileChange = file.lastModified(); } // okay, "transactions" are applied, now perform the search for (Entry<String, Map<String, Row>> table : super.getContactSource().database.entrySet()) { for (Map.Entry<String, Row> e : table.getValue().entrySet()) { if (e.getKey().endsWith(getContactSource().defaultScope)) { readEntry(e.getValue()); } } } super.stopped(true); } catch (FileNotFoundException e) { logger.warn("Could not open address book", e); } catch (Exception e) { logger.warn("Could not parse " + file, e); } } /** * Processes a database row by matching it against the query and adding it * to the result set if it matched. * * @param r The database row representing a contact. */ private void readEntry(Row r) { // match the pattern against this contact boolean hadMatch = false; for (Alias value : r.getAliases().values()) { if (value != null && (super.query.matcher(value.getValue()).find() || super .phoneNumberMatches(value.getValue()))) { hadMatch = true; break; } } // nope, didn't match, ignore if (!hadMatch) { return; } List<ContactDetail> details = new LinkedList<ContactDetail>(); // e-mail(s) for (String email : getPropertySet(r, "PrimaryEmail", "SecondEmail", "DefaultEmail")) { ContactDetail detail = new ContactDetail(email, Category.Email); detail.addSupportedOpSet(OperationSetPersistentPresence.class); details.add(detail); } // phone number(s) this.addPhoneDetail(details, r, "HomePhone", SubCategory.Home); this.addPhoneDetail(details, r, "WorkPhone", SubCategory.Work); this.addPhoneDetail(details, r, "CellularNumber", SubCategory.Mobile); // and the dispaly name String displayName = r.getValue("DisplayName"); if (StringUtils.isNullOrEmpty(displayName, true)) { displayName = r.getValue("LastName"); if (displayName != null) { displayName = displayName.trim(); } String firstName = r.getValue("FirstName"); if (!StringUtils.isNullOrEmpty(firstName, true)) { displayName = firstName + " " + displayName; } } // create the contact and add it to the results GenericSourceContact sc = new GenericSourceContact(super.getContactSource(), displayName, details); addQueryResult(sc); } /** * Adds a "Phone" {@link ContactDetail} to a query contact. * * @param details The {@link List} of {@link ContactDetail}s to which the * details is added. * @param r The source database row of the contact. * @param property The source database property name to add as a detail. * @param category The Phone-{@link SubCategory} for the phone number to * add. */ private void addPhoneDetail(List<ContactDetail> details, Row r, String property, SubCategory category) { String phone = r.getValue(property); if (StringUtils.isNullOrEmpty(phone, true)) { return; } phone = ThunderbirdActivator.getPhoneNumberI18nService().normalize(phone); ContactDetail detail = new ContactDetail(phone, ContactDetail.Category.Phone, new ContactDetail.SubCategory[] { category }); detail.addSupportedOpSet(OperationSetBasicTelephony.class); detail.addSupportedOpSet(OperationSetPersistentPresence.class); details.add(detail); } /** * Gets a set of non-empty properties from the source database row. * * @param r The source database row to process. * @param properties The property-names to extract. * @return A set of non-empty properties from the source database row. */ private Set<String> getPropertySet(Row r, String... properties) { Set<String> validValues = new HashSet<String>(properties.length); for (String prop : properties) { String value = r.getValue(prop); if (!StringUtils.isNullOrEmpty(value, true)) { validValues.add(value); } } return validValues; } }