/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.core.plugins.risks; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.apache.commons.lang3.StringUtils; import org.roda.core.data.common.RodaConstants; import org.roda.core.data.common.RodaConstants.PreservationEventType; import org.roda.core.data.exceptions.AlreadyExistsException; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.v2.LiteOptionalWithCause; import org.roda.core.data.v2.formats.Format; import org.roda.core.data.v2.index.filter.AndFiltersParameters; import org.roda.core.data.v2.index.filter.Filter; import org.roda.core.data.v2.index.filter.FilterParameter; import org.roda.core.data.v2.index.filter.OrFiltersParameters; import org.roda.core.data.v2.index.filter.SimpleFilterParameter; import org.roda.core.data.v2.index.sort.SortParameter; import org.roda.core.data.v2.index.sort.Sorter; import org.roda.core.data.v2.index.sublist.Sublist; import org.roda.core.data.v2.ip.AIPState; import org.roda.core.data.v2.ip.File; import org.roda.core.data.v2.ip.IndexedFile; import org.roda.core.data.v2.ip.metadata.FileFormat; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.PluginParameter; import org.roda.core.data.v2.jobs.PluginType; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.jobs.Report.PluginState; import org.roda.core.data.v2.risks.Risk; import org.roda.core.data.v2.risks.RiskIncidence; import org.roda.core.data.v2.validation.ValidationException; import org.roda.core.index.IndexService; import org.roda.core.model.ModelService; import org.roda.core.plugins.AbstractPlugin; import org.roda.core.plugins.Plugin; import org.roda.core.plugins.PluginException; import org.roda.core.plugins.RODAObjectProcessingLogic; import org.roda.core.plugins.orchestrate.JobPluginInfo; import org.roda.core.plugins.orchestrate.SimpleJobPluginInfo; import org.roda.core.plugins.plugins.PluginHelper; import org.roda.core.storage.StorageService; import org.roda.core.util.IdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Format Missing Representation Information risk assessment plugin. * * @author Rui Castro <rui.castro@gmail.com> */ public class FormatMissingRepresentationInformationPlugin extends AbstractPlugin<File> { /** Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(FormatMissingRepresentationInformationPlugin.class); /** Plugin version. */ private static final String VERSION = "1.1.1"; /** Risk ID. */ private static final String RISK_ID = "urn:FormatMissingRepresentationInformation:r1"; /** Plugin parameter value 'true'. */ private static final String PARAM_VALUE_TRUE = "true"; /** Plugin parameter ID 'mimetype'. */ private static final String MIMETYPE = "mimetype"; /** Plugin parameter name 'mimetype'. */ private static final String MIMETYPE_NAME = "Mimetype"; /** Plugin parameter 'mimetype'. */ private static final PluginParameter PARAM_MIMETYPE = new PluginParameter(MIMETYPE, MIMETYPE_NAME, PluginParameter.PluginParameterType.BOOLEAN, PARAM_VALUE_TRUE, false, false, "Check Mimetype?"); /** Plugin parameter ID 'pronom'. */ private static final String PRONOM = "pronom"; /** Plugin parameter name 'pronom'. */ private static final String PRONOM_NAME = "PRONOM"; /** Plugin parameter 'pronom'. */ private static final PluginParameter PARAM_PRONOM = new PluginParameter(PRONOM, PRONOM_NAME, PluginParameter.PluginParameterType.BOOLEAN, PARAM_VALUE_TRUE, false, false, "Check PRONOM Unique Identifier (PUID)?"); /** Plugin parameter ID 'format'. */ private static final String FORMAT_DESIGNATION = "format"; /** Plugin parameter name 'format'. */ private static final String FORMAT_DESIGNATION_NAME = "Format designation"; /** Plugin parameter 'format'. */ private static final PluginParameter PARAM_FORMAT_DESIGNATION = new PluginParameter(FORMAT_DESIGNATION, FORMAT_DESIGNATION_NAME, PluginParameter.PluginParameterType.BOOLEAN, PARAM_VALUE_TRUE, false, false, "Check Format designation name and version?"); /** Plugin parameter ID 'extension'. */ private static final String EXTENSION = "extension"; /** Plugin parameter name 'extension'. */ private static final String EXTENSION_NAME = "Extension"; /** Plugin parameter 'extension'. */ private static final PluginParameter PARAM_EXTENSION = new PluginParameter(EXTENSION, EXTENSION_NAME, PluginParameter.PluginParameterType.BOOLEAN, PARAM_VALUE_TRUE, false, false, "Check extension?"); /** Plugin parameter ID 'matchOne'. */ private static final String MATCH_ONE = "matchOne"; /** Plugin parameter 'matchOne'. */ private static final PluginParameter PARAM_MATCH_ONE = new PluginParameter(MATCH_ONE, "Match (at least) one Format type", PluginParameter.PluginParameterType.BOOLEAN, "false", false, false, "Don't create risk incidence(s) if at least one of the selected format types is found."); @Override public void init() throws PluginException { // do nothing } @Override public void shutdown() { // do nothing } @Override public String getName() { return "Format missing representation information risk assessment"; } @Override public String getDescription() { return "Check file format (Mimetype, PRONOM, Extension and Format designation) in the Format Registry. " + "If file format is not present in the Format Registry, it creates a new risk called " + "“Comprehensive representation information is missing for some files in the repository“ " + "and assigns the file to that risk in the Risk register."; } @Override public String getVersionImpl() { return VERSION; } @Override public Report execute(final IndexService index, final ModelService model, final StorageService storage, final List<LiteOptionalWithCause> liteList) throws PluginException { return PluginHelper.processObjects(this, new RODAObjectProcessingLogic<File>() { @Override public void process(IndexService index, ModelService model, StorageService storage, Report report, Job cachedJob, SimpleJobPluginInfo jobPluginInfo, Plugin<File> plugin, File object) { executeOnFile(object, index, model, jobPluginInfo, report, cachedJob); } }, index, model, storage, liteList); } @Override public Plugin<File> cloneMe() { return new FormatMissingRepresentationInformationPlugin(); } @Override public PluginType getType() { return PluginType.MISC; } @Override public List<PluginParameter> getParameters() { return Arrays.asList(PARAM_MIMETYPE, PARAM_PRONOM, PARAM_FORMAT_DESIGNATION, PARAM_EXTENSION, PARAM_MATCH_ONE); } @Override public boolean areParameterValuesValid() { return true; } @Override public PreservationEventType getPreservationEventType() { return PreservationEventType.RISK_MANAGEMENT; } @Override public String getPreservationEventDescription() { return getName(); } @Override public String getPreservationEventSuccessMessage() { return "File format has Representation information."; } @Override public String getPreservationEventFailureMessage() { return "File format doesn't have Representation information."; } @Override public Report beforeAllExecute(final IndexService index, final ModelService model, final StorageService storage) throws PluginException { // do nothing return null; } @Override public Report afterAllExecute(final IndexService index, final ModelService model, final StorageService storage) throws PluginException { // do nothing return null; } @Override public List<String> getCategories() { return Collections.singletonList(RodaConstants.PLUGIN_CATEGORY_RISK_MANAGEMENT); } @Override public List<Class<File>> getObjectClasses() { return Collections.singletonList(File.class); } /** * Check the mimetype? * * @return <code>true</code> if plugin should search for the mimetype in the * Format Registry, <code>false</code> otherwise. */ private boolean checkMimetype() { return PARAM_VALUE_TRUE.equalsIgnoreCase(getParameterValues().get(MIMETYPE)); } /** * Check the PRONOM Unique Identifier (PUID)? * * @return <code>true</code> if plugin should search for the PRONOM Unique * Identifier (PUID) in the Format Registry, <code>false</code> * otherwise. */ private boolean checkPronom() { return PARAM_VALUE_TRUE.equalsIgnoreCase(getParameterValues().get(PRONOM)); } /** * Check the format designation? * * @return <code>true</code> if plugin should search for the format * designation in the Format Registry, <code>false</code> otherwise. */ private boolean checkFormatDesignation() { return PARAM_VALUE_TRUE.equalsIgnoreCase(getParameterValues().get(FORMAT_DESIGNATION)); } /** * Check the extension? * * @return <code>true</code> if plugin should search for the extension in the * Format Registry, <code>false</code> otherwise. */ private boolean checkExtension() { return PARAM_VALUE_TRUE.equalsIgnoreCase(getParameterValues().get(EXTENSION)); } /** * Match at least one format type? * * @return <code>true</code> if plugin should not create a risk incidence if * at least one format type is found, <code>false</code> otherwise. */ private boolean matchAtLeastOneFormatType() { return PARAM_VALUE_TRUE.equalsIgnoreCase(getParameterValues().get(MATCH_ONE)); } /** * Execute verifications on a single {@link File}. * * @param file * the {@link File}. * @param index * the {@link IndexService}. * @param model * the {@link ModelService}. * @param jobPluginInfo * the {@link JobPluginInfo} * @param jobReport * the {@link Report}. */ private void executeOnFile(final File file, final IndexService index, final ModelService model, final JobPluginInfo jobPluginInfo, final Report jobReport, final Job job) { LOGGER.debug("Processing File {}", file.getId()); final String fileId = IdUtils.getFileId(file); final Report fileReport = PluginHelper.initPluginReportItem(this, fileId, File.class, AIPState.ACTIVE); PluginHelper.updatePartialJobReport(this, model, fileReport, false, job); try { final FileFormat fileFormat = index .retrieve(IndexedFile.class, fileId, RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN).getFileFormat(); final FileFormatResult result; if (matchAtLeastOneFormatType()) { result = new MatchOneResult(fileFormat, index); } else { result = new MatchAllResult(fileFormat, index); } if (result.isMissingAttributes()) { jobPluginInfo.incrementObjectsProcessedWithFailure(); fileReport.setPluginState(PluginState.FAILURE); } else if (result.isInRisk()) { jobPluginInfo.incrementObjectsProcessedWithFailure(); fileReport.setPluginState(PluginState.FAILURE); openRisk(file, fileReport, index, model); } else { jobPluginInfo.incrementObjectsProcessedWithSuccess(); fileReport.setPluginState(PluginState.SUCCESS); mitigateRisk(file, fileReport, index, model); } addToReportDetails(fileReport, result.toString()); } catch (final NotFoundException | GenericException e) { final String message = String.format("Error retrieving IndexedFile for File %s (%s)", file.getId(), fileId); LOGGER.debug(message, e); jobPluginInfo.incrementObjectsProcessedWithFailure(); fileReport.setPluginState(PluginState.FAILURE); addToReportDetails(fileReport, message); } try { PluginHelper.createPluginEvent(this, file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId(), model, index, null, null, fileReport.getPluginState(), "", true); } catch (final RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException | ValidationException | AlreadyExistsException e) { final String message = "Could not create plugin event for file " + file.getId(); addToReportDetails(fileReport, message); fileReport.setPluginState(PluginState.FAILURE); LOGGER.error(message, e); } jobReport.addReport(fileReport); PluginHelper.updatePartialJobReport(this, model, fileReport, true, job); } /** * Mitigate risk. * * @param file * the {@link File}. * @param fileReport * the {@link File} {@link Report}. * @param index * the {@link IndexService}. * @param model * the {@link ModelService}. */ private void mitigateRisk(final File file, final Report fileReport, final IndexService index, final ModelService model) { try { final RiskIncidence incidence = findUnmitigatedIncidence(file, index); incidence.setStatus(RiskIncidence.INCIDENCE_STATUS.MITIGATED); model.updateRiskIncidence(incidence, true); final String message = String.format("Incidence of risk \"%s\" mitigated for File \"%s\"", RISK_ID, file.getId()); addToReportDetails(fileReport, message); LOGGER.info(message); } catch (final NotFoundException e) { LOGGER.trace(e.getMessage(), e); final String message = String.format( "File \"%s\" doesn't have an unmitigated incidence of risk \"%s\". Nothing to mitigate.", file.getId(), RISK_ID); LOGGER.info(message); } catch (final GenericException e) { final String message = String.format( "An internal error occurred searching incidence of risk \"%s\" for File \"%s\". (Technical details: %s).", RISK_ID, file, e.getMessage()); LOGGER.warn(message, e); addToReportDetails(fileReport, message); fileReport.setPluginState(PluginState.PARTIAL_SUCCESS); } } /** * Open (unmitigated) risk incidence. * * @param file * the {@link File} with the risk. * @param fileReport * the {@link File} {@link Report}. * @param index * the {@link IndexService}. * @param model * the {@link ModelService}. */ private void openRisk(final File file, final Report fileReport, final IndexService index, final ModelService model) { try { final RiskIncidence incidence = findUnmitigatedIncidence(file, index); model.updateRiskIncidence(incidence, true); final String message = String.format( "Unmitigated incidence of risk \"%s\" already exists for File \"%s\". Refreshing it.", RISK_ID, file.getId()); LOGGER.info(message); } catch (final NotFoundException e) { LOGGER.trace(e.getMessage(), e); createRiskIncidence(file, fileReport, model); final String message = String.format("Unmitigated incidence of risk \"%s\" created for File \"%s\".", RISK_ID, file.getId()); addToReportDetails(fileReport, message); LOGGER.info(message); } catch (final GenericException e) { final String message = String.format( "An internal error occurred searching incidence of risk \"%s\" for File \"%s\". (Technical details: %s).", RISK_ID, file.getId(), e.getMessage()); LOGGER.warn(message, e); addToReportDetails(fileReport, message); fileReport.setPluginState(PluginState.PARTIAL_SUCCESS); } } /** * Find an unmitigated {@link RiskIncidence} for the specified {@link File}. * * @param file * the {@link File}. * @param index * the {@link IndexService}. * @return the {@link RiskIncidence}. * @throws NotFoundException * if an unmitigated risk incidence doesn't exist. * @throws GenericException * if some error occurred. */ private RiskIncidence findUnmitigatedIncidence(final File file, final IndexService index) throws NotFoundException, GenericException { final Filter filter = new Filter( Arrays.asList(new SimpleFilterParameter("status", RiskIncidence.INCIDENCE_STATUS.UNMITIGATED.toString()), new SimpleFilterParameter("riskId", RISK_ID), new SimpleFilterParameter("aipId", file.getAipId()), new SimpleFilterParameter("representationId", file.getRepresentationId()), new SimpleFilterParameter("fileId", file.getId()))); // new SimpleFilterParameter("filePath", file.getPath()) try { final List<RiskIncidence> results = index.find(RiskIncidence.class, filter, new Sorter(new SortParameter("detectedOn", true)), new Sublist(0, 1), new ArrayList<>()).getResults(); if (results.isEmpty()) { throw new NotFoundException("Couldn't find RiskIncidence matching filter " + filter); } else { return results.get(0); } } catch (final RequestNotValidException e) { throw new GenericException(e.getMessage(), e); } } /** * Create a {@link RiskIncidence}. * * @param file * the {@link File}. * @param report * the {@link Report}. * @param model * the {@link ModelService}. */ private void createRiskIncidence(final File file, final Report report, final ModelService model) { try { final Risk risk = PluginHelper.createRiskIfNotExists(model, RISK_ID, getClass().getClassLoader()); final RiskIncidence incidence = new RiskIncidence(); incidence.setDetectedOn(new Date()); incidence.setDetectedBy(this.getName()); incidence.setRiskId(RISK_ID); incidence.setAipId(file.getAipId()); incidence.setRepresentationId(file.getRepresentationId()); incidence.setFilePath(file.getPath()); incidence.setFileId(file.getId()); incidence.setObjectClass(File.class.getSimpleName()); incidence.setStatus(RiskIncidence.INCIDENCE_STATUS.UNMITIGATED); incidence.setSeverity(risk.getPreMitigationSeverityLevel()); incidence.setDescription("Comprehensive representation information is missing."); model.createRiskIncidence(incidence, false); } catch (final RequestNotValidException | AuthorizationDeniedException | AlreadyExistsException | NotFoundException | GenericException e) { final String message = String.format("Error creating risk %s incidence for File %s", RISK_ID, file.getId()); addToReportDetails(report, message); report.setPluginState(PluginState.FAILURE); LOGGER.error(message, e); } } /** * Add a message to the {@link Report} details. * * @param report * the {@link Report}. * @param message * the message to add. */ private void addToReportDetails(final Report report, final String message) { final String pluginDetails; if (StringUtils.isBlank(report.getPluginDetails())) { pluginDetails = message; } else { pluginDetails = String.format("%n%n%s", message); } report.addPluginDetails(pluginDetails); } /** * This interface is a {@link FileFormat} risk assessment result. */ interface FileFormatResult { /** * Check if all needed attributes exist in {@link FileFormat}. * * @return <code>true</code> if all attributes exist, <code>false</code> * otherwise. */ boolean isMissingAttributes(); /** * Check if a {@link FileFormat} is in risk. A {@link FileFormat} is in risk * if the Format registry doesn't have a {@link Format} with it's attributes * (format designation, mimetype, pronom). * * @return <code>true</code> if the {@link FileFormat} is in risk, * <code>false</code> otherwise. */ boolean isInRisk(); } /** * This is an abstract implementation of a {@link FileFormatResult}. * * @author Rui Castro <rui.castro@gmail.com> */ abstract class AbstractResult implements FileFormatResult { /** * The {@link FileFormat}. */ private final FileFormat fileFormat; /** * Constructor. * * @param format * the {@link FileFormat}. */ AbstractResult(final FileFormat format) { this.fileFormat = format; } /** * The {@link List} of {@link FormatResult} matching this {@link FileFormat} * . * * @return a {@link List<FormatResult>}. */ abstract List<FormatResult> formatResults(); @Override public boolean isInRisk() { return this.formatResults().isEmpty(); } @Override public String toString() { StringBuilder str = new StringBuilder(); if (isMissingAttributes()) { str.append("File does not have required information (Format designation, MIME type, PRONOM or Extension), " + "to be able to find Format representation information."); } else if (formatResults().isEmpty()) { str.append(getPreservationEventFailureMessage()); } else { str.append(String.format("%s%n%n", getPreservationEventSuccessMessage())); for (FormatResult result : this.formatResults()) { str.append(String.format("%s%n", result)); } } return str.toString(); } /** * The {@link List} of {@link AttributeCheck} for this result. * * @return a {@link List<AttributeCheck>}. */ List<AttributeCheck> attributeChecks() { return attributeChecks(true); } /** * The {@link List} of {@link AttributeCheck} for this result. * * @param present * default value for present attribute. * * @return a {@link List<AttributeCheck>}. */ List<AttributeCheck> attributeChecks(final boolean present) { final List<AttributeCheck> checks = new ArrayList<>(); if (checkMimetype()) { checks.add(new AttributeCheck(MIMETYPE_NAME, fileFormat.getMimeType(), present, new SimpleFilterParameter(RodaConstants.FORMAT_MIMETYPES, fileFormat.getMimeType()), isMissingMimetype())); } if (checkPronom()) { checks.add(new AttributeCheck(PRONOM_NAME, fileFormat.getPronom(), present, new SimpleFilterParameter(RodaConstants.FORMAT_PRONOMS, fileFormat.getPronom()), isMissingPronom())); } if (checkExtension()) { checks.add(new AttributeCheck(EXTENSION_NAME, fileFormat.getExtension(), present, new SimpleFilterParameter(RodaConstants.FORMAT_EXTENSIONS, fileFormat.getExtension()), isMissingExtension())); } if (checkFormatDesignation()) { final FilterParameter mainName = new SimpleFilterParameter(RodaConstants.FORMAT_NAME, fileFormat.getFormatDesignationName()); final FilterParameter alternativeName = new SimpleFilterParameter(RodaConstants.FORMAT_ALTERNATIVE_DESIGNATIONS, fileFormat.getFormatDesignationName()); final FilterParameter name = new OrFiltersParameters(Arrays.asList(mainName, alternativeName)); final FilterParameter version = new SimpleFilterParameter(RodaConstants.FORMAT_VERSIONS, fileFormat.getFormatDesignationVersion()); final FilterParameter nameAndVersion = new AndFiltersParameters(Arrays.asList(name, version)); final FilterParameter nameAndVersionOrName = new OrFiltersParameters(Arrays.asList(nameAndVersion, name)); if (StringUtils.isBlank(fileFormat.getFormatDesignationVersion())) { checks.add(new AttributeCheck(FORMAT_DESIGNATION_NAME, fileFormat.getFormatDesignationName(), present, name, isMissingFormatDesignation())); } else { checks .add(new AttributeCheck(FORMAT_DESIGNATION_NAME, String.format("%s\" with version: \"%s", fileFormat.getFormatDesignationName(), fileFormat.getFormatDesignationVersion()), present, nameAndVersionOrName, isMissingFormatDesignation())); } } return checks; } boolean isMissingFormatDesignation() { return checkFormatDesignation() && (StringUtils.isBlank(this.fileFormat.getFormatDesignationName()) && StringUtils.isBlank(this.fileFormat.getFormatDesignationVersion())); } boolean isMissingMimetype() { return checkMimetype() && StringUtils.isBlank(this.fileFormat.getMimeType()); } boolean isMissingPronom() { return checkPronom() && StringUtils.isBlank(this.fileFormat.getPronom()); } boolean isMissingExtension() { return checkExtension() && StringUtils.isBlank(this.fileFormat.getExtension()); } } /** * Implementation of {@link FileFormatResult} that has to match all * attributes. * * @author Rui Castro <rui.castro@gmail.com> */ class MatchAllResult extends AbstractResult { /** * List of {@link FormatResult}. */ private List<FormatResult> formatResults = null; /** * The {@link IndexService}. */ private final IndexService indexService; /** * Constructor. * * @param format * the {@link FileFormat}. * @param indexService * the {@link IndexService}. */ MatchAllResult(final FileFormat format, final IndexService indexService) { super(format); this.indexService = indexService; } @Override public boolean isMissingAttributes() { return isMissingFormatDesignation() || isMissingMimetype() || isMissingPronom() || isMissingExtension(); } @Override List<FormatResult> formatResults() { if (this.formatResults == null) { this.formatResults = new ArrayList<>(); final List<FilterParameter> filterParams = new ArrayList<>(); final List<AttributeCheck> checks = attributeChecks(); checks.forEach(check -> filterParams.add(check.getFilterParameter())); final Iterator<Format> formats = this.indexService .findAll(Format.class, new Filter(filterParams), new ArrayList<>()).iterator(); formats.forEachRemaining(format -> this.formatResults.add(new FormatResult(format, checks))); } return this.formatResults; } } /** * Implementation of {@link FileFormatResult} that only needs to match one * attribute. * * @author Rui Castro <rui.castro@gmail.com> */ class MatchOneResult extends AbstractResult { /** * List of {@link FormatResult}. */ private List<FormatResult> formatResults = null; /** * The {@link IndexService}. */ private final IndexService indexService; /** * Constructor. * * @param format * the {@link FileFormat}. * @param indexService * the {@link IndexService}. */ MatchOneResult(final FileFormat format, final IndexService indexService) { super(format); this.indexService = indexService; } @Override public boolean isMissingAttributes() { return isMissingFormatDesignation() && isMissingMimetype() && isMissingPronom() && isMissingExtension(); } @Override List<FormatResult> formatResults() { if (this.formatResults == null) { this.formatResults = new ArrayList<>(); for (AttributeCheck check : attributeChecks(true)) { if (!check.isMissingAttribute()) { final Iterator<Format> formats = this.indexService .findAll(Format.class, new Filter(check.getFilterParameter()), new ArrayList<>()).iterator(); formats.forEachRemaining( format -> this.formatResults.add(new FormatResult(format, Collections.singletonList(check)))); } } consolidateResults(); } return this.formatResults; } /** * Consolidate {@link List} of {@link FormatResult}. Merge repeated * {@link FormatResult}s and set default {@link AttributeCheck} (with * <code>present = false</code>) to all {@link FormatResult} and all * attributes. */ private void consolidateResults() { final Map<String, FormatResult> map = new TreeMap<>(); for (FormatResult result : this.formatResults) { if (!map.containsKey(result.id())) { map.put(result.id(), new FormatResult(result.format, attributeChecks(false))); } map.get(result.id()).merge(result); } this.formatResults.clear(); this.formatResults.addAll(map.values()); } } /** * This is a {@link FileFormat} match result. * * @author Rui Castro <rui.castro@gmail.com> */ class FormatResult { /** * The {@link Format}. */ private final Format format; /** * The {@link Map} of {@link AttributeCheck} used to find the {@link Format} * . */ private final Map<String, AttributeCheck> checks; /** * Constructor. * * @param format * the {@link Format}. * @param checks * the {@link List<AttributeCheck>}. */ FormatResult(final Format format, final List<AttributeCheck> checks) { this.format = format; this.checks = new TreeMap<>(); checks.forEach(c -> this.checks.put(c.name, c)); } /** * Return the unique identifier for this {@link FormatResult} which is the * same as the inner {@link Format}. * * @return a {@link String} with the unique identifier. */ String id() { return this.format.getId(); } @Override public String toString() { StringBuilder str = new StringBuilder( String.format("Format \"%s\" (%s)%n", this.format.getName(), this.format.getId())); for (Map.Entry<String, AttributeCheck> entry : this.checks.entrySet()) { str.append(String.format("\t%s%n", entry.getValue())); } return str.toString(); } /** * Merge another {@link FormatResult} checks into this checks. * * @param result * the {@link FormatResult} to merge. */ void merge(final FormatResult result) { this.checks.putAll(result.checks); } } /** * This is a attribute that was used to match a {@link Format}. * * @author Rui Castro <rui.castro@gmail.com> */ class AttributeCheck { /** * The name. */ private final String name; /** * The value. */ private final String value; /** * Is it present? */ private final boolean present; /** * The {@link FilterParameter} corresponding to this check. */ private final FilterParameter filterParameter; /** * Is the value missing? */ private final boolean missing; /** * Constructor. * * @param name * The name. * @param value * The value. * @param present * Is it present? * @param filterParameter * The {@link FilterParameter} corresponding to this check. * @param missing * Is the value missing? */ AttributeCheck(final String name, final String value, final boolean present, final FilterParameter filterParameter, final boolean missing) { this.name = name; this.value = value; this.present = present; this.filterParameter = filterParameter; this.missing = missing; } FilterParameter getFilterParameter() { return filterParameter; } @Override public String toString() { return String.format("%s %s \"%s\"", this.present ? "has" : "does NOT have", this.name, this.value); } boolean isMissingAttribute() { return missing; } } }