package de.dpa.oss.metadata.mapper; import com.adobe.xmp.XMPException; import com.google.common.io.ByteStreams; import de.dpa.oss.metadata.mapper.common.XmlUtils; import de.dpa.oss.metadata.mapper.common.YAXPathExpressionException; import de.dpa.oss.metadata.mapper.imaging.ChainedImageMetadataOperations; import de.dpa.oss.metadata.mapper.imaging.ConfigToExifToolTagNames; import de.dpa.oss.metadata.mapper.imaging.ConfigValidationException; import de.dpa.oss.metadata.mapper.imaging.G2ToMetadataMapper; import de.dpa.oss.metadata.mapper.imaging.backend.exiftool.ExifToolIntegrationException; import de.dpa.oss.metadata.mapper.imaging.backend.exiftool.ExifToolWrapper; import de.dpa.oss.metadata.mapper.imaging.backend.exiftool.taginfo.TagGroupItem; import de.dpa.oss.metadata.mapper.imaging.backend.exiftool.taginfo.TagInfo; import de.dpa.oss.metadata.mapper.imaging.common.ImageMetadata; import de.dpa.oss.metadata.mapper.imaging.configuration.generated.IIMMapping; import de.dpa.oss.metadata.mapper.imaging.configuration.generated.MappingType; import de.dpa.oss.metadata.mapper.imaging.configuration.generated.XMPMappingTargetType; import de.dpa.oss.metadata.mapper.imaging.configuration.generated.XMPMapsTo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import javax.xml.bind.JAXBException; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; /** * This class offers the metadata mapper functionality. Its purpose is to construct the desired functionality and to * call it. Typical use: * <pre> * MetadataMapper.modifyImageAt( "image.jpg").withXMLDocument(xmldoc).withDefaultMapping().mapToImage(resultImage) * </pre> * * @author oliver langer */ public class MetadataMapper { private static Logger logger = LoggerFactory.getLogger(MetadataMapper.class); private static TagInfo tagInfo = null; private final InputStream imageInputStream; private Document xmlDocument = null; private MappingType mapping = null; private boolean emptyTargetTagGroups = false; private Map<String, String> tagGroupsToClear = null; private boolean removeAllTagGroups = false; private Path temporaryDirectory = null; public static MetadataMapper modifyImageAt(final String pathToSourceImage) throws FileNotFoundException { return new MetadataMapper(pathToSourceImage); } public static MetadataMapper modifyImageAt(final InputStream sourceImage) throws FileNotFoundException { return new MetadataMapper(sourceImage); } public static MetadataMapper explainMapping() { return new MetadataMapper(); } private MetadataMapper(final String pathToSourceImage) throws FileNotFoundException { this(new FileInputStream(pathToSourceImage)); } private MetadataMapper() { imageInputStream = null; } private MetadataMapper(final InputStream imageInputStream) { this.imageInputStream = imageInputStream; } public MetadataMapper useTemporaryDirectory( final String temporaryDirectoryPath ) { temporaryDirectory = Paths.get(temporaryDirectoryPath); if( !Files.isDirectory(temporaryDirectory)) { throw new IllegalArgumentException( "Given path does not point to a directory:" + temporaryDirectoryPath); } return this; } public MetadataMapper xmlDocument(final String pathToXMLDocument) throws Exception { logger.debug("Reading XML document :" + pathToXMLDocument); String xmlSource = new String(ByteStreams.toByteArray(new FileInputStream(pathToXMLDocument))); xmlDocument = XmlUtils.toDocument(xmlSource); return this; } public MetadataMapper xmlDocument(final Document xmlDocument) { this.xmlDocument = xmlDocument; return this; } public MetadataMapper useDefaultMapping() throws JAXBException { logger.debug("Using DefaultMapping"); this.mapping = MetadataMapperConfigReader.getDefaultMapping(); return this; } /** * @deprecated inheritance not supported in future */ public MetadataMapper useDefaultMappingOverridenBy(final String pathToMapping) throws FileNotFoundException, JAXBException { logger.debug("Overriding default mapping by mapping definitions defined in:" + pathToMapping); this.mapping = MetadataMapperConfigReader.getDefaultConfigOverridenBy(pathToMapping); return this; } public MetadataMapper useCustomMapping( final Path mappingConfigFile ) throws FileNotFoundException, JAXBException { logger.debug( "Using custom mapping"); this.mapping = MetadataMapperConfigReader.readCustomConfig(new FileInputStream(mappingConfigFile.toFile())); return this; } public MetadataMapper useDefaultMappingOverridenBy(final MappingType customMapping) { logger.debug("Overriding default mapping by mapping:" + customMapping.getName()); this.mapping = customMapping; return this; } /** * Expects a list of metadata mapper to remove before mapping is being performed * * @param tagGroupsToClear list of tagGroupsToClear to erase. exiftool format is expected. that is: GROUP:TAG * E.g. IPTC:ALL, XMP:XMP-dc */ public MetadataMapper tagGroupsToRemoveBeforeMapping(final Map<String, String> tagGroupsToClear) { this.tagGroupsToClear = tagGroupsToClear; return this; } public MetadataMapper removeAllTagGroups() { this.removeAllTagGroups = true; return this; } public MetadataMapper emptyTargetTagGroups() { this.emptyTargetTagGroups = true; return this; } public void executeMapping(final String pathToResultingImage) throws IOException, XMPException, YAXPathExpressionException, ExifToolIntegrationException { try (FileOutputStream fileOutputStream = new FileOutputStream(pathToResultingImage)) { executeMapping(fileOutputStream); } } /** * Note: does not close the output stream */ public void executeMapping(final OutputStream imageOutput) throws YAXPathExpressionException, XMPException, IOException, ExifToolIntegrationException { if (imageOutput == null || xmlDocument == null || mapping == null) { throw new IllegalArgumentException("At least one parameter (image input, image output, source xml, mapping) is missing"); } ImageMetadata imageMetadata = new ImageMetadata(); new G2ToMetadataMapper(mapping).mapToImageMetadata(xmlDocument, imageMetadata); ChainedImageMetadataOperations chainedImageMetadataOperations = ChainedImageMetadataOperations .modifyImage(imageInputStream, imageOutput); if( temporaryDirectory != null ) { chainedImageMetadataOperations.useTemporaryDirectory(temporaryDirectory); } if (emptyTargetTagGroups) { chainedImageMetadataOperations.clearMetadataGroupsReferredByMapping(imageMetadata); } if (removeAllTagGroups) { chainedImageMetadataOperations.clearAllMetadataGroups(); } else if (tagGroupsToClear != null && tagGroupsToClear.size() > 0) { chainedImageMetadataOperations.clearMetadataGroups(tagGroupsToClear); } chainedImageMetadataOperations.setMetadata(imageMetadata); chainedImageMetadataOperations.execute(ExifToolWrapper.anExifTool().build()); } /** * Used to dump the mapping. The dump includes the xpaths, the selected values and the list of target * fields. * * Note: this implementation is in experimental state. The XMP-part is not dumped out accordingly. */ public void explainMapping( final Writer writer ) throws IOException, YAXPathExpressionException { if (writer == null || xmlDocument == null || mapping == null) { throw new IllegalArgumentException("At least one parameter (writer, source xml, mapping) is missing"); } ImageMetadata imageMetadata = new ImageMetadata(); new G2ToMetadataMapper(mapping).explainMapToImageMetadata(xmlDocument, writer); } protected static synchronized TagInfo getExifToolTagInfo() throws ExifToolIntegrationException { if (tagInfo == null) { // todo cache somehow tagInfo = ExifToolWrapper.anExifTool().build().getSupportedTagsOfGroups(); } return tagInfo; } /** * Validates the given configuration. In case of an validation error an {@link ConfigValidationException} is thrown * * @throws ExifToolIntegrationException * @throws ConfigValidationException */ public static void validate(final MappingType mappingToValidate) throws ExifToolIntegrationException, ConfigValidationException { TagInfo tagInfo = MetadataMapper.getExifToolTagInfo(); for (MappingType.Metadata metadata : mappingToValidate.getMetadata()) { if (metadata.getXmp() != null) { for (XMPMapsTo xmpMapsTo : metadata.getXmp().getMapsTo()) { validateXMPElements(metadata, tagInfo, xmpMapsTo, ""); } } if (metadata.getIim() != null) { for (IIMMapping.MapsTo iimMapsTo : metadata.getIim().getMapsTo()) { if (!tagInfo.hasGroupContainingTagWithId(ConfigToExifToolTagNames.IPTC_APPLICATION_TAGGROUP_NAME, iimMapsTo.getDataset().toString())) { throw new ConfigValidationException(metadata.getName(), ConfigToExifToolTagNames.IPTC_APPLICATION_TAGGROUP_NAME, iimMapsTo.getField()); } else { TagGroupItem tagInfoById = tagInfo.getGroupByName(ConfigToExifToolTagNames.IPTC_APPLICATION_TAGGROUP_NAME) .getTagInfoById(iimMapsTo.getDataset().toString()); if (!tagInfoById.getName().equals(iimMapsTo.getField())) { throw new ConfigValidationException(metadata.getName(), ConfigToExifToolTagNames.IPTC_APPLICATION_TAGGROUP_NAME, iimMapsTo.getField()); } } } } } } /** * exiftool uses a compound name scheme: A member of a struct has its structure name as prefix. */ private static void validateXMPElements(final MappingType.Metadata metadata, final TagInfo tagInfo, final XMPMapsTo mappingItem, final String strPrefix) throws ConfigValidationException { String targetNamespace = mappingItem.getTargetNamespace(); String tagGroupname = ConfigToExifToolTagNames.getTagGroupnameByConfigNamespace(targetNamespace); if (!tagInfo.hasGroupContainingTagWithId(tagGroupname, strPrefix + mappingItem.getField())) { throw new ConfigValidationException(metadata.getName(), mappingItem.getTargetNamespace(), strPrefix + mappingItem.getField()); } if (mappingItem.getTargetType() == XMPMappingTargetType.STRUCT || mappingItem.getTargetType() == XMPMappingTargetType.SEQUENCE || mappingItem.getTargetType() == XMPMappingTargetType.BAG) { final String prefix; if (mappingItem.getTargetType() == XMPMappingTargetType.STRUCT) { prefix = strPrefix + mappingItem.getField(); } else { prefix = strPrefix; } for (XMPMapsTo child : mappingItem.getMapsTo()) { validateXMPElements(metadata, tagInfo, child, prefix); } } } }