Skip to content

Commit

Permalink
Add automatic media job type compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
incjo authored and wmorland committed Sep 5, 2022
1 parent 23fd34b commit 6191281
Show file tree
Hide file tree
Showing 26 changed files with 1,049 additions and 21 deletions.
1 change: 0 additions & 1 deletion portability-spi-transfer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ dependencies {
compile project(':portability-api-launcher')

compile('org.apache.commons:commons-lang3:3.11')

}

configurePublication(project)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.datatransferproject.spi.transfer.provider;

import com.google.common.base.Preconditions;
import java.util.Objects;
import java.util.Optional;
import org.datatransferproject.spi.transfer.types.ContinuationData;
import org.datatransferproject.types.common.models.DataModel;
Expand All @@ -9,6 +10,7 @@
* The result of an item export operation, after retries.
*/
public class ExportResult<T extends DataModel> {

public static final ExportResult CONTINUE = new ExportResult(ResultType.CONTINUE);
public static final ExportResult END = new ExportResult(ResultType.CONTINUE);

Expand All @@ -31,7 +33,7 @@ public ExportResult(ResultType type) {
/**
* Ctor.
*
* @param type the result type
* @param type the result type
* @param exportedData the exported data
*/
public ExportResult(ResultType type, T exportedData) {
Expand All @@ -51,8 +53,8 @@ public <R extends DataModel> ExportResult<R> copyWithExportedData(R replacementD
/**
* Ctor.
*
* @param type the result type
* @param exportedData the exported data
* @param type the result type
* @param exportedData the exported data
* @param continuationData continuation information
*/
public ExportResult(ResultType type, T exportedData, ContinuationData continuationData) {
Expand Down Expand Up @@ -99,6 +101,26 @@ private void verifyNonErrorResultType(ResultType type) {
Preconditions.checkArgument(!type.equals(ResultType.ERROR), mustHaveThrowable);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ExportResult)) {
return false;
}
ExportResult<?> that = (ExportResult<?>) o;
return type == that.type &&
Objects.equals(exportedData, that.exportedData) &&
Objects.equals(continuationData, that.continuationData) &&
Objects.equals(throwable, that.throwable);
}

@Override
public int hashCode() {
return Objects.hash(type, exportedData, continuationData, throwable);
}

/**
* Result types.
*/
Expand All @@ -115,6 +137,16 @@ public enum ResultType {
/**
* Indicates an unrecoverable error was raised.
*/
ERROR
ERROR;

public static ResultType merge(ResultType t1, ResultType t2) {
if (t1 == ResultType.ERROR || t2 == ResultType.ERROR) {
return ResultType.ERROR;
}
if (t1 == ResultType.CONTINUE || t2 == ResultType.CONTINUE) {
return ResultType.CONTINUE;
}
return ResultType.END;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.datatransferproject.spi.transfer.provider;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

/** The result of an item import operation, after retries. */
public class ImportResult {
Expand Down Expand Up @@ -52,6 +55,33 @@ public ImportResult(
this.bytes = bytes;
}

/** Returns a new result that's a combination of the two given results. */
public static ImportResult merge(ImportResult ir1, ImportResult ir2) {
if (ir1.getType() == ResultType.ERROR) {
return ir1;
}
if (ir2.getType() == ResultType.ERROR) {
return ir2;
}
ImportResult res = new ImportResult(ResultType.OK);
res.bytes = Stream.of(ir1.getBytes(), ir2.getBytes())
.filter(Optional::isPresent)
.map(Optional::get)
.reduce(Long::sum);
res.counts = mergeCounts(ir1, ir2);
return res;
}

private static Optional<Map<String, Integer>> mergeCounts(ImportResult ir1, ImportResult ir2) {
if (ir1.counts.isPresent() && ir2.counts.isPresent()) {
Map<String, Integer> map = new HashMap<>(ir1.counts.get());
ir2.counts.get().forEach((k, v) -> map.merge(k, v, Integer::sum));
return Optional.of(map);
} else {
return ir1.counts.isPresent() ? ir1.counts : ir2.counts;
}
}

/** Returns the type of result. */
public ResultType getType() {
return type;
Expand Down Expand Up @@ -91,6 +121,26 @@ public ImportResult copyWithBytes(Long bytes) {
return new ImportResult(type, throwable, counts, Optional.ofNullable(bytes));
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ImportResult)) {
return false;
}
ImportResult that = (ImportResult) o;
return type == that.type &&
Objects.equals(throwable, that.throwable) &&
Objects.equals(counts, that.counts) &&
Objects.equals(bytes, that.bytes);
}

@Override
public int hashCode() {
return Objects.hash(type, throwable, counts, bytes);
}

/** Result types. */
public enum ResultType {
/** Indicates a successful import. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package org.datatransferproject.spi.transfer.provider;

import java.util.function.Function;
import org.datatransferproject.spi.transfer.extension.TransferExtension;
import org.datatransferproject.spi.transfer.provider.converter.AnyToAnyExporter;
import org.datatransferproject.spi.transfer.provider.converter.AnyToAnyImporter;
import org.datatransferproject.spi.transfer.provider.converter.MediaExporterDecorator;
import org.datatransferproject.spi.transfer.provider.converter.MediaImporterDecorator;
import org.datatransferproject.types.common.models.ContainerResource;
import org.datatransferproject.types.common.models.media.MediaContainerResource;
import org.datatransferproject.types.common.models.photos.PhotosContainerResource;
import org.datatransferproject.types.common.models.videos.VideosContainerResource;

/**
* Provides a compatibility layer between adapter types. E.g. when asking for a Media adapter and it
* isn't available, it will combine Photo and Video adapters on the fly to seamlessly support the
* request.
*/
public class TransferCompatibilityProvider {

private static final String PHOTOS = "PHOTOS";
private static final String VIDEOS = "VIDEOS";
private static final String MEDIA = "MEDIA";

public Exporter getCompatibleExporter(TransferExtension extension, String jobType) {
Exporter<?, ?> exporter = getExporterOrNull(extension, jobType);
if (exporter != null) {
return exporter;
}

switch (jobType) {
case MEDIA:
exporter = getMediaExporter(extension);
break;
case PHOTOS:
exporter = getPhotosExporter(extension);
break;
case VIDEOS:
exporter = getVideosExporter(extension);
break;
}
if (exporter == null) {
return extension.getExporter(jobType); // preserve original exception
}
return exporter;
}

public Importer getCompatibleImporter(TransferExtension extension, String jobType) {
Importer<?, ?> importer = getImporterOrNull(extension, jobType);
if (importer != null) {
return importer;
}

switch (jobType) {
case MEDIA:
importer = getMediaImporter(extension);
break;
case PHOTOS:
importer = getPhotosImporter(extension);
break;
case VIDEOS:
importer = getVideosImporter(extension);
break;
}
if (importer == null) {
return extension.getImporter(jobType);
}
return importer;
}

private Importer<?, ?> getVideosImporter(TransferExtension extension) {
Importer media = getImporterOrNull(extension, MEDIA);
if (media == null) {
return null;
}
return new AnyToAnyImporter<>(media, MediaContainerResource::videoToMedia);
}

private Importer<?, ?> getPhotosImporter(TransferExtension extension) {
Importer media = getImporterOrNull(extension, MEDIA);
if (media == null) {
return null;
}
return new AnyToAnyImporter<>(media, MediaContainerResource::photoToMedia);
}

private Importer<?, ?> getMediaImporter(TransferExtension extension) {
Importer<?, ?> photo = getImporterOrNull(extension, PHOTOS);
Importer<?, ?> video = getImporterOrNull(extension, VIDEOS);
if (photo == null || video == null) {
return null;
}
return new MediaImporterDecorator(photo, video);
}

private Exporter<?, ?> getVideosExporter(TransferExtension extension) {
Exporter media = getExporterOrNull(extension, MEDIA);
if (media == null) {
return null;
}
Function<ContainerResource, ContainerResource> converter = (cr) ->
(cr instanceof VideosContainerResource) ?
MediaContainerResource.videoToMedia((VideosContainerResource) cr) : cr;
return new AnyToAnyExporter<>(media, MediaContainerResource::mediaToVideo, converter);
}

private Exporter<?, ?> getPhotosExporter(TransferExtension extension) {
Exporter media = getExporterOrNull(extension, MEDIA);
if (media == null) {
return null;
}
Function<ContainerResource, ContainerResource> converter = (cr) ->
(cr instanceof PhotosContainerResource) ?
MediaContainerResource.photoToMedia((PhotosContainerResource) cr) : cr;
return new AnyToAnyExporter<>(media, MediaContainerResource::mediaToPhoto, converter);
}

private Exporter<?, ?> getMediaExporter(TransferExtension extension) {
Exporter<?, ?> photo = getExporterOrNull(extension, PHOTOS);
Exporter<?, ?> video = getExporterOrNull(extension, VIDEOS);
if (photo == null || video == null) {
return null;
}
return new MediaExporterDecorator(photo, video);
}

private Exporter<?, ?> getExporterOrNull(TransferExtension extension, String jobType) {
// TODO: Don't use exceptions for control flow. Have a way to query supported adapters
try {
return extension.getExporter(jobType);
} catch (Exception e) {
return null;
}
}

private Importer<?, ?> getImporterOrNull(TransferExtension extension, String jobType) {
try {
return extension.getImporter(jobType);
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.datatransferproject.spi.transfer.provider.converter;

import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import org.datatransferproject.spi.transfer.provider.ExportResult;
import org.datatransferproject.spi.transfer.provider.Exporter;
import org.datatransferproject.types.common.ExportInformation;
import org.datatransferproject.types.common.models.ContainerResource;
import org.datatransferproject.types.transfer.auth.AuthData;

/**
* Allows flexible bridging between adapters of different types with compatible functionality.
*
* @param <From> The container type supported by the available exporter.
* @param <To> The container type that's desired.
*/
public class AnyToAnyExporter<
AD extends AuthData,
From extends ContainerResource,
To extends ContainerResource> implements Exporter<AD, To> {

private final Exporter<AD, From> exporter;
private final Function<From, To> containerResourceConverter;
private final Function<ContainerResource, ContainerResource> exportInformationConverter;

/**
* @param exporter existing exporter
* @param containerResourceConverter function converting between the existing and desired
* containers.
* @param exportInformationConverter converter that's used to adapt the export information that
* goes into the export call. It's required because exporters
* often support various container resources as part of export
* information. E.g. photo exporters support
* DateRangeContainerResource, while media adapters might only
* support MediaContainerResource.
*/
public AnyToAnyExporter(Exporter<AD, From> exporter,
Function<From, To> containerResourceConverter,
Function<ContainerResource, ContainerResource> exportInformationConverter) {
this.exporter = exporter;
this.containerResourceConverter = containerResourceConverter;
this.exportInformationConverter = exportInformationConverter;
}

/**
* @param exporter existing exporter
* @param containerResourceConverter function converting between the existing and desired
* containers.
*/
public AnyToAnyExporter(Exporter<AD, From> exporter,
Function<From, To> containerResourceConverter) {
this(exporter, containerResourceConverter, Function.identity());
}

@Override
public ExportResult<To> export(UUID jobId, AD authData, Optional<ExportInformation> exportInfo)
throws Exception {
Optional<ExportInformation> infoWithConvertedResource =
exportInfo.map(
(ei) ->
ei.copyWithResource(exportInformationConverter.apply(ei.getContainerResource())));
ExportResult<From> originalResult = exporter.export(jobId, authData, infoWithConvertedResource);
return originalResult.copyWithExportedData(
containerResourceConverter.apply(originalResult.getExportedData()));
}
}
Loading

0 comments on commit 6191281

Please sign in to comment.