/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE and NOTICE files at the root of the source * tree and available online at * * http://www.dspace.org/license/ */ package org.dspace.app.util; import java.sql.SQLException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.content.*; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.apache.log4j.Logger; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; import org.dspace.core.ConfigurationManager; import java.io.File; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.factory.HandleServiceFactory; import org.jdom.Element; /** * Configuration and mapping for Google Scholar output metadata * @author Sands Fish * */ @SuppressWarnings("deprecation") public class GoogleMetadata { private final static Logger log = Logger.getLogger(GoogleMetadata.class); protected static final String GOOGLE_PREFIX = "google."; protected ItemService itemService; protected Item item; protected String itemURL; // Configuration keys and fields protected static Map<String, String> configuredFields = new HashMap<String, String>(); // Google field names (e.g. citation_fieldname) and formatted metadata // values protected ListMultimap<String, String> metadataMappings = ArrayListMultimap.create(); protected final String TITLE = "citation_title"; protected final String JOURNAL_TITLE = "citation_journal_title"; protected final String PUBLISHER = "citation_publisher"; protected final String AUTHORS = "citation_author"; protected final String DATE = "citation_date"; protected final String VOLUME = "citation_volume"; protected final String ISSUE = "citation_issue"; protected final String FIRSTPAGE = "citation_firstpage"; protected final String LASTPAGE = "citation_lastpage"; protected final String DOI = "citation_doi"; protected final String PMID = "citation_pmid"; protected final String ABSTRACT = "citation_abstract_html_url"; protected final String FULLTEXT = "citation_fulltext_html_url"; protected final String PDF = "citation_pdf_url"; protected final String ISSN = "citation_issn"; protected final String ISBN = "citation_isbn"; protected final String LANGUAGE = "citation_language"; protected final String KEYWORDS = "citation_keywords"; protected final String CONFERENCE = "citation_conference"; protected final String DISSERTATION_ID = "identifiers.dissertation"; protected final String DISSERTATION_NAME = "citation_dissertation_name"; protected final String DISSERTATION_INSTITUTION = "citation_dissertation_institution"; protected final String PATENT_ID = "identifiers.patent"; protected final String PATENT_NUMBER = "citation_patent_number"; protected final String PATENT_COUNTRY = "citation_patent_country"; protected final String TECH_REPORT_ID = "identifiers.technical_report"; protected final String TECH_REPORT_NUMBER = "citation_technical_report_number"; protected final String TECH_REPORT_INSTITUTION = "citation_technical_report_institution"; protected final int SINGLE = 0; protected final int MULTI = 1; protected final int ALL_FIELDS_IN_OPTION = 2; // Load configured fields from google-metadata.properties static { File loadedFile = null; URL url = null; InputStream is = null; String googleConfigFile = ConfigurationManager .getProperty("google-metadata.config"); log.info("Using [" + googleConfigFile + "] for Google Metadata configuration"); loadedFile = new File(googleConfigFile); try { url = loadedFile.toURL(); } catch (MalformedURLException mux) { log.error("Can't find Google Metadata configuration file: " + googleConfigFile, mux); } Properties properties = new Properties(); try { is = url.openStream(); properties.load(is); } catch (IOException iox) { log.error("Could not read Google Metadata configuration file: " + googleConfigFile, iox); } Enumeration propertyNames = properties.propertyNames(); while (propertyNames.hasMoreElements()) { String key = ((String) propertyNames.nextElement()).trim(); if (key.startsWith(GOOGLE_PREFIX)) { String name = key.substring(GOOGLE_PREFIX.length()); String field = properties.getProperty(key); if (null != name && !name.equals("") && null != field && !field.equals("")) { configuredFields.put(name.trim(), field.trim()); } } } if (log.isDebugEnabled()) { logConfiguration(); } } /** * Dump Metadata field mapping to log * */ public static void logConfiguration() { log.debug("Google Metadata Configuration Mapping:"); for (String name : configuredFields.keySet()) { log.debug(" " + name + " => " + configuredFields.get(name)); } } /** * Wrap the item, parse all configured fields and generate metadata field * values. * * @param context context * @param item The item being viewed to extract metadata from * @throws SQLException if database error */ public GoogleMetadata(Context context, Item item) throws SQLException { // Hold onto the item in case we need to refresh a stale parse this.item = item; this.itemService = ContentServiceFactory.getInstance().getItemService(); itemURL = HandleServiceFactory.getInstance().getHandleService().resolveToURL(context, item.getHandle()); parseItem(); } /** * Add a single metadata value to the Google field, defaulting to the * first-encountered instance of the field for this Item. * * @param fieldName * metadata field name * @return successful? */ protected boolean addSingleField(String fieldName) { String config = configuredFields.get(fieldName); if (null == config || config.equals("")) { return false; } if (log.isDebugEnabled()) { log.debug("Processing " + fieldName); } if (config.equals("$handle")) { if (null != itemURL && !itemURL.equals("")) { metadataMappings.put(fieldName, itemURL); return true; } else { return false; } } if (config.equals("$simple-pdf")) { String pdf_url = getPDFSimpleUrl(item); if (pdf_url.length() > 0) { metadataMappings.put(fieldName, pdf_url); return true; } else { return false; } } MetadataValue v = resolveMetadataField(config); if (null != v && (null != v.getValue()) && !v.getValue().trim().equals("")) { metadataMappings.put(fieldName, v.getValue()); return true; } else { // No values found return false; } } /** * A singular version of resolveMetadata to return only one field value * instead of an aggregate. * * @param configFilter * list of DC metadata fields separated by "|" characters * @return The first configured match of metadata field for the item. */ protected MetadataValue resolveMetadataField(String configFilter) { ArrayList<MetadataValue> fields = resolveMetadata(configFilter, SINGLE); if (null != fields && fields.size() > 0) { return fields.get(0); } return null; } /** * A plural version of resolveMetadata for aggregate fields. * * @param configFilter * list of DC metadata fields separated by "|" characters * @return Aggregate of all matching metadata fields configured in the first * option field-set to return any number of filter matches. */ protected ArrayList<MetadataValue> resolveMetadataFields(String configFilter) { ArrayList<MetadataValue> fields = resolveMetadata(configFilter, MULTI); if (null != fields && fields.size() > 0) { return fields; } return null; } /** * Aggregate an array of DCValues present on the current item that pass the * configuration filter. * * @param configFilter * list of DC metadata fields separated by "|" characters * @param returnType * GoogleMetadata.SINGLE / GoogleMetadata.MULTI / GoogleMetadata.ALL_FIELDS_IN_OPTION * @return Array of configuration to item-field matches */ protected ArrayList<MetadataValue> resolveMetadata(String configFilter, int returnType) { if (null == configFilter || configFilter.trim().equals("") || !configFilter.contains(".")) { log.error("The configuration string [" + configFilter + "] is invalid."); return null; } else { configFilter = configFilter.trim(); } ArrayList<ArrayList<String>> parsedOptions = parseOptions(configFilter); if (log.isDebugEnabled()) { log.debug("Resolved Fields For This Item Per Configuration Filter:"); for (int i = 0; i < parsedOptions.size(); i++) { ArrayList<String> optionFields = parsedOptions.get(i); log.debug("Option " + (i + 1) + ":"); for (String f : optionFields) { log.debug("{" + f + "}"); } } } // Iterate through each configured option's field-set until // we have a match. for (ArrayList<String> optionFields : parsedOptions) { int optionMatches = 0; String[] components; List<MetadataValue> values; ArrayList<MetadataValue> resolvedFields = new ArrayList<MetadataValue>(); for (String field : optionFields) { components = parseComponents(field); values = itemService.getMetadata(item, components[0], components[1], components[2], Item.ANY); if (values.size() > 0) { for (MetadataValue v : values) { resolvedFields.add(v); if (returnType == SINGLE) { if (!resolvedFields.isEmpty()) { if (log.isDebugEnabled()) { log .debug("Resolved Field Value For This Item:"); for (MetadataValue r : resolvedFields) { log.debug("{" + r.getValue() + "}"); } } return resolvedFields; } } } } } // If the item had any of the fields contained in this option, // return them, otherwise move on to the next option's field-set. if (!resolvedFields.isEmpty()) { if (log.isDebugEnabled()) { log.debug("Resolved Field Values For This Item:"); for (MetadataValue v : resolvedFields) { log.debug("{" + v.getValue() + "}"); } } // Check to see if this is a full option match if (ALL_FIELDS_IN_OPTION == returnType) { if (resolvedFields.size() == optionMatches) { return resolvedFields; } // Otherwise, if there are any matches for the option, // return them. } else if (MULTI == returnType) { return resolvedFields; } } } return null; } /** * Parse first-match path of metadata field-group options for the given * configuration. * * @param configFilter * list of DC metadata fields separated by "|" characters * @return array of parsed options or null */ protected ArrayList<ArrayList<String>> parseOptions(String configFilter) { ArrayList<String> options = new ArrayList<String>(); ArrayList<ArrayList<String>> parsedOptions = new ArrayList<ArrayList<String>>(); if (null == configFilter || configFilter.equals("")) { return null; } if (configFilter.contains("|")) { String[] configOptions = configFilter.split("\\|"); for (String option : configOptions) { options.add(option.trim()); } } else { options = new ArrayList<String>(); options.add(configFilter); } // Parse first-match path options. The first option (field-set) // to match fields present in the item is used. ArrayList<String> parsedFields; // Parse the fields for each field-set in order. for (String option : options) { ArrayList<String> fields; parsedFields = new ArrayList<String>(); if (option.contains(",")) { fields = parseFields(option); } else { fields = new ArrayList<String>(); fields.add(option); } // Parse field list for this field-set, expanding any wildcards. for (String field : fields) { if (field.contains("*")) { ArrayList<String> wc = parseWildcard(field); for (String wcField : wc) { if (!parsedFields.contains(wcField)) { parsedFields.add(wcField); } } } else { if (!parsedFields.contains(field)) { parsedFields.add(field); } } } parsedOptions.add(parsedFields); } if (null != parsedOptions) { return parsedOptions; } else { return null; } } /** * Build a Vector of fields that can be added to when expanding wildcards. * * @param configString * - Value of one metadata field configuration * @return A vector of raw field configurations. */ protected ArrayList<String> parseFields(String configString) { ArrayList<String> fields = new ArrayList<String>(); for (String field : configString.split("\\,")) { fields.add(field.trim()); } return fields; } /** * Pull apart an individual field structure. * * @param field * The configured field for one metadata field map * @return Schema, Element, Qualifier of metadata field */ protected String[] parseComponents(String field) { int index = 0; String[] components = new String[3]; for (String c : field.split("\\.")) { components[index] = c.trim(); index++; } return components; } /** * Expand any wildcard characters to an array of all matching fields for * this item. No order consistency is implied. * * @param field * The field identifier containing a wildcard character. * @return Expanded field list. */ protected ArrayList<String> parseWildcard(String field) { if (!field.contains("*")) { return null; } else { String[] components = parseComponents(field); for (int i = 0; i < components.length; i++) { if (components[i].trim().equals("*")) { components[i] = Item.ANY; } } List<MetadataValue> allMD = itemService.getMetadata(item, components[0], components[1], components[2], Item.ANY); ArrayList<String> expandedDC = new ArrayList<String>(); for (MetadataValue v : allMD) { // De-dup multiple occurrences of field names in item if (!expandedDC.contains(buildFieldName(v))) { expandedDC.add(buildFieldName(v)); } } if (log.isDebugEnabled()) { log.debug("Field Names From Expanded Wildcard \"" + field + "\""); for (String v : expandedDC) { log.debug(" " + v); } } return expandedDC; } } /** * Construct metadata field name out of Metadatum components * * @param v * The Metadatum to construct a name for. * @return The complete metadata field name. */ protected String buildFieldName(MetadataValue v) { StringBuilder name = new StringBuilder(); MetadataField metadataField = v.getMetadataField(); MetadataSchema metadataSchema = v.getMetadataField().getMetadataSchema(); name.append(metadataSchema.getName()).append(".").append(metadataField.getElement()); if (null != metadataField.getQualifier()) { name.append("." + metadataField.getQualifier()); } return name.toString(); } /** * Using metadata field mappings contained in the loaded configuration, * parse through configured metadata fields, building valid Google metadata * value strings. Field names and values contained in metadataMappings. * */ protected void parseItem() { // TITLE addSingleField(TITLE); // AUTHORS (multi) addMultipleValues(AUTHORS); // DATE addSingleField(DATE); // ISSN addSingleField(ISSN); // ISBN addSingleField(ISBN); // JOURNAL_TITLE addSingleField(JOURNAL_TITLE); // VOLUME addSingleField(VOLUME); // ISSUE addSingleField(ISSUE); // FIRSTPAGE addSingleField(FIRSTPAGE); // LASTPAGE addSingleField(LASTPAGE); // DOI addSingleField(DOI); // PMID addSingleField(PMID); // ABSTRACT_HTML_URL ('$handle' variable substitution if present) addSingleField(ABSTRACT); // FULLTEXT_HTML_URL ('$handle' variable substitution if present) addSingleField(FULLTEXT); // PDF_URL ('$handle' variable substitution if present) addSingleField(PDF); // LANGUAGE addSingleField(LANGUAGE); // KEYWORDS (multi) addAggregateValues(KEYWORDS, ";"); // CONFERENCE addSingleField(CONFERENCE); // Dissertations if (itemIsDissertation()) { if (log.isDebugEnabled()) { log.debug("ITEM TYPE: DISSERTATION"); } addSingleField(DISSERTATION_NAME); addSingleField(DISSERTATION_INSTITUTION); } // Patents if (itemIsPatent()) { if (log.isDebugEnabled()) { log.debug("ITEM TYPE: PATENT"); } addSingleField(PATENT_NUMBER); // Use config value for patent country. Should be a literal. String countryConfig = configuredFields.get(PATENT_COUNTRY); if (null != countryConfig && !countryConfig.trim().equals("")) { metadataMappings.put(PATENT_COUNTRY, countryConfig.trim()); } addSingleField(PUBLISHER); } // Tech Reports if (itemIsTechReport()) { if (log.isDebugEnabled()) { log.debug("ITEM TYPE: TECH REPORT"); } addSingleField(TECH_REPORT_NUMBER); addSingleField(TECH_REPORT_INSTITUTION); } if (!itemIsDissertation() && !itemIsTechReport()) { // PUBLISHER addSingleField(PUBLISHER); } } /** * Fetch retaining the order of the values for any given key in which they * where added (like authors). * * Usage: {@code GoogleMetadata gmd = new GoogleMetadata(item); for(Entry<String, * String> mapping : googlemd.getMappings()) ...} * * @return Iterable of metadata fields mapped to Google-formatted values */ public Collection<Entry<String, String>> getMappings() { return metadataMappings.entries(); } /** * Produce meta elements that can easily be put into the head. * @return List of elements */ public List<Element> disseminateList() { List<Element> metas = new ArrayList<Element>(); for (Entry<String, String> m : getMappings()) { Element e = new Element("meta"); e.setNamespace(null); e.setAttribute("name", m.getKey()); e.setAttribute("content", m.getValue()); metas.add(e); } return metas; } // Getters for individual metadata fields... /** * @return the citation_title */ public List<String> getTitle() { return metadataMappings.get(TITLE); } /** * @return the citation_journal_title */ public List<String> getJournalTitle() { return metadataMappings.get(JOURNAL_TITLE); } /** * @return the citation_publisher */ public List<String> getPublisher() { return metadataMappings.get(PUBLISHER); } /** * @return the citation_authors */ public List<String> getAuthors() { return metadataMappings.get(AUTHORS); } /** * @return the citation_date */ public List<String> getDate() { return metadataMappings.get(DATE); } /** * @return the citation_volume */ public List<String> getVolume() { return metadataMappings.get(VOLUME); } /** * @return the citation_issue */ public List<String> getIssue() { return metadataMappings.get(ISSUE); } /** * @return the citation_firstpage */ public List<String> getFirstpage() { return metadataMappings.get(FIRSTPAGE); } /** * @return the citation_lastpage */ public List<String> getLastpage() { return metadataMappings.get(LASTPAGE); } /** * @return the citation_doi */ public List<String> getDOI() { return metadataMappings.get(DOI); } /** * @return the citation_pmid */ public List<String> getPmid() { return metadataMappings.get(PMID); } /** * @return the citation_abstract_html_url */ public List<String> getAbstractHTMLURL() { return metadataMappings.get(ABSTRACT); } /** * @return the citation_fulltext_html_url */ public List<String> getFulltextHTMLURL() { return metadataMappings.get(FULLTEXT); } /** * @return the citation_pdf_url */ public List<String> getPDFURL() { return metadataMappings.get(PDF); } /** * @return the citation_issn */ public List<String> getISSN() { return metadataMappings.get(ISSN); } /** * @return the citation_isbn */ public List<String> getISBN() { return metadataMappings.get(ISBN); } /** * @return the citation_language */ public List<String> getLanguage() { return metadataMappings.get(LANGUAGE); } /** * @return the citation_keywords */ public List<String> getKeywords() { return metadataMappings.get(KEYWORDS); } /** * @return the citation_conference */ public List<String> getConference() { return metadataMappings.get(CONFERENCE); } /** * @return the citation_dissertation_name */ public List<String> getDissertationName() { return metadataMappings.get(DISSERTATION_NAME); } /** * @return the citation_dissertation_institution */ public List<String> getDissertationInstitution() { return metadataMappings.get(DISSERTATION_INSTITUTION); } /** * @return the citation_patent_number */ public List<String> getPatentNumber() { return metadataMappings.get(PATENT_NUMBER); } /** * @return the citation_patent_country */ public List<String> getPatentCountry() { return metadataMappings.get(PATENT_COUNTRY); } /** * @return the citation_technical_report_number */ public List<String> getTechnicalReportNumber() { return metadataMappings.get(TECH_REPORT_NUMBER); } /** * @return the citation_technical_report_institution */ public List<String> getTechnicalReportInstitution() { return metadataMappings.get(TECH_REPORT_INSTITUTION); } /** * Gets the URL to a PDF using a very basic strategy by assuming that the PDF * is in the default content bundle, and that the item only has one public bitstream * and it is a PDF. * * @param item item to get PDF URL from * @return URL that the PDF can be directly downloaded from */ protected String getPDFSimpleUrl(Item item) { try { Bitstream bitstream = findLinkableFulltext(item); if (bitstream != null) { StringBuilder path = new StringBuilder(); path.append(ConfigurationManager.getProperty("dspace.url")); if (item.getHandle() != null) { path.append("/bitstream/"); path.append(item.getHandle()); path.append("/"); path.append(bitstream.getSequenceID()); } else { path.append("/retrieve/"); path.append(bitstream.getID()); } path.append("/"); path.append(Util.encodeBitstreamName(bitstream.getName(), Constants.DEFAULT_ENCODING)); return path.toString(); } } catch (UnsupportedEncodingException ex) { log.debug(ex.getMessage()); } catch (SQLException ex) { log.debug(ex.getMessage()); } return ""; } /** * A bitstream is considered linkable fulltext when it is either * <ul> * <li>the item's only bitstream (in the ORIGINAL bundle); or</li> * <li>the primary bitstream</li> * </ul> * Additionally, this bitstream must be publicly viewable. * * @param item * bitstream's parent item * @return a linkable bitstream or null if none found * @throws SQLException if database error */ protected Bitstream findLinkableFulltext(Item item) throws SQLException { Bitstream bestSoFar = null; int bitstreamCount = 0; List<Bundle> contentBundles = itemService.getBundles(item, "ORIGINAL"); for (Bundle bundle : contentBundles) { List<Bitstream> bitstreams = bundle.getBitstreams(); for (Bitstream candidate : bitstreams) { if (candidate.equals(bundle.getPrimaryBitstream())) { // is primary -> use this one if (isPublic(candidate)) { return candidate; } } else { if (bestSoFar == null && isPublic(candidate)) { //if bestSoFar is null but the candidate is not public you don't use it and try to find another bestSoFar = candidate; } } } } return bestSoFar; } /** * Find out whether bitstream is readable by the public. * * @param bitstream * the target bitstream * @return whether bitstream is readable by the Anonymous group */ protected boolean isPublic(Bitstream bitstream) { if (bitstream == null) { return false; } boolean result = false; Context context = null; try { context = new Context(); result = AuthorizeServiceFactory.getInstance().getAuthorizeService().authorizeActionBoolean(context, bitstream, Constants.READ, true); } catch (SQLException e) { log.error("Cannot determine whether bitstream is public, assuming it isn't. bitstream_id=" + bitstream.getID(), e); } return result; } /** * * * @param field * to aggregate all values of in a matching option * @param delimiter * to delimit field values with */ protected void addAggregateValues(String field, String delimiter) { String authorConfig = configuredFields.get(field); ArrayList<MetadataValue> fields = resolveMetadataFields(authorConfig); if (null != fields && !fields.isEmpty()) { StringBuilder fieldMetadata = new StringBuilder(); int count = 0; for (MetadataValue metadataValue : fields) { fieldMetadata.append(metadataValue.getValue()); if (count < fields.size() - 1) { fieldMetadata.append(delimiter).append(" "); count++; } } metadataMappings.put(field, fieldMetadata.toString()); } } /** * If metadata field contains multiple values, then add each value to the map separately * @param FIELD * metadata field */ protected void addMultipleValues(String FIELD) { String fieldConfig = configuredFields.get(FIELD); ArrayList<MetadataValue> fields = resolveMetadataFields(fieldConfig); if (null != fields && !fields.isEmpty()) { for (MetadataValue field : fields) { //TODO if this is author field, first-name first metadataMappings.put(FIELD, field.getValue()); } } } /** * Determine, based on config values, if this item is a dissertation. * * @return boolean */ protected boolean itemIsDissertation() { String dConfig = configuredFields.get(DISSERTATION_ID); if (null == dConfig || dConfig.trim().equals("")) { return false; } else { return identifyItemType(dConfig); } } /** * Determine, based on config values, if this item is a patent. * * @return boolean */ protected boolean itemIsPatent() { String dConfig = configuredFields.get(PATENT_ID); if (null == dConfig || dConfig.trim().equals("")) { return false; } else { return identifyItemType(dConfig); } } /** * Determine, based on config values, if this item is a tech report. * * @return boolean */ protected boolean itemIsTechReport() { String dConfig = configuredFields.get(TECH_REPORT_ID); if (null == dConfig || dConfig.trim().equals("")) { return false; } else { return identifyItemType(dConfig); } } /** * Identifies if this item matches a particular configuration of fields and * values for those fields to identify the type based on a type- cataloging * metadata practice. * * @param dConfig * configured fields (from google-metadata.properties) * @return item matches configuration */ protected boolean identifyItemType(String dConfig) { // FIXME: Shouldn't have to parse identifiers for every identification. ArrayList<ArrayList<String>> options = parseOptions(dConfig); HashMap<String, ArrayList<String>> mdPairs = new HashMap<String, ArrayList<String>>(); // Parse field/value pairs from field identifier string for (ArrayList<String> option : options) { String pair = option.get(0); String[] parsedPair = pair.split("\\:"); if (2 == parsedPair.length) { // If we've encountered this field before, add the value to the // list if (mdPairs.containsKey(parsedPair[0].trim())) { mdPairs.get(parsedPair[0].trim()).add(parsedPair[1]); if (log.isDebugEnabled()) { log.debug("Registering Type Identifier: " + parsedPair[0] + " => " + parsedPair[1]); } } else { // Otherwise, add it as the first occurrence of this field ArrayList<String> newField = new ArrayList<String>(); newField.add(parsedPair[1].trim()); mdPairs.put(parsedPair[0].trim(), newField); if (log.isDebugEnabled()) { log.debug("Registering Type Identifier: " + parsedPair[0] + " => " + parsedPair[1]); } } } else { log.error("Malformed field identifier name/value pair"); } } // Build config string without values, only field names StringBuilder sb = new StringBuilder(); for (String value : mdPairs.keySet()) { sb.append(value).append(" | "); } // Check resolved/present metadata fields against configured values ArrayList<MetadataValue> presentMD = resolveMetadataFields(sb.toString()); if (null != presentMD && presentMD.size() != 0) { for (MetadataValue v : presentMD) { String fieldName = buildFieldName(v); if (mdPairs.containsKey(fieldName)) { for (String configValue : mdPairs.get(fieldName)) { if (configValue.equals(v.getValue())) { return true; } } } } } return false; } }