pl.edu.icm.unity.saml.metadata.cfg.AsyncExternalLogoFileDownloader Maven / Gradle / Ivy
/*
* Copyright (c) 2018 Bixbit - Krzysztof Benedyczak. All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package pl.edu.icm.unity.saml.metadata.cfg;
import org.apache.commons.io.FileExistsException;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import pl.edu.icm.unity.MessageSource;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.config.UnityServerConfiguration;
import pl.edu.icm.unity.engine.api.files.RemoteFileData;
import pl.edu.icm.unity.engine.api.files.URIAccessService;
import pl.edu.icm.unity.engine.api.utils.ExecutorsService;
import pl.edu.icm.unity.saml.sp.config.BaseSamlConfiguration.RemoteMetadataSource;
import pl.edu.icm.unity.saml.sp.config.TrustedIdPConfiguration;
import pl.edu.icm.unity.saml.sp.config.TrustedIdPKey;
import pl.edu.icm.unity.saml.sp.config.TrustedIdPs;
import pl.edu.icm.unity.types.translation.ProfileType;
import pl.edu.icm.unity.types.translation.TranslationProfile;
import xmlbeans.org.oasis.saml2.metadata.EntitiesDescriptorDocument;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Base64.getDecoder;
import static java.util.Collections.synchronizedSet;
@Component
public class AsyncExternalLogoFileDownloader
{
private static final Logger log = Log.getLogger(Log.U_SERVER_SAML, AsyncExternalLogoFileDownloader.class);
private static final String STAGING = "staging";
private final ExecutorService executorService;
private final URIAccessService uriAccessService;
private final MetadataToSPConfigConverter converter;
private final String workspaceDir;
private final String defaultLocale;
private final Duration socketReadTimeout;
private final Duration connectionTimeout;
private final Set currentlyDownloadingFederation = synchronizedSet(new HashSet<>());
public AsyncExternalLogoFileDownloader(UnityServerConfiguration conf, MessageSource msg, URIAccessService uriAccessService,
ExecutorsService executorsService, MetadataToSPConfigConverter converter)
{
workspaceDir = LogoFilenameUtils.getLogosWorkspace(conf);
executorService = executorsService.getExecutionService();
defaultLocale = msg.getLocale().toString();
this.uriAccessService = uriAccessService;
this.converter = converter;
this.socketReadTimeout = Duration.ofMillis(conf.getIntValue(UnityServerConfiguration.BULK_FILES_DOWNLOAD_TIMEOUT));
this.connectionTimeout = Duration.ofMillis(conf.getIntValue(UnityServerConfiguration.BULK_FILES_CONNECTION_TIMEOUT));
}
@SuppressWarnings("unchecked")
public void downloadLogoFilesAsync(EntitiesDescriptorDocument entitiesDescriptorDocument, String httpsTruststore)
{
String federationId = entitiesDescriptorDocument.getEntitiesDescriptor().getID();
if (!currentlyDownloadingFederation.add(federationId))
{
log.info("Logos of federation {} are being downloaded, won't start a new downloading process", federationId);
return;
}
CompletableFuture>[] savedFilesNamesFutures;
try
{
RemoteMetadataSource metadataSource = RemoteMetadataSource.builder()
.withTranslationProfile(new TranslationProfile("mock", "description", ProfileType.INPUT, List.of()))
.withUrl("url")
.withRefreshInterval(Duration.ZERO)
.build();
TrustedIdPs trustedIdPs = converter.convertToTrustedIdPs(entitiesDescriptorDocument, metadataSource);
log.info("Will download logos for {} IdPs of federation {}", trustedIdPs.getKeys().size(),
entitiesDescriptorDocument.getEntitiesDescriptor().getName());
savedFilesNamesFutures = trustedIdPs.getEntrySet().stream()
.map(entry -> CompletableFuture.supplyAsync(() -> downloadFiles(entry, httpsTruststore), executorService))
.toArray(CompletableFuture[]::new);
}
catch (Exception e)
{
currentlyDownloadingFederation.remove(federationId);
log.error("This exception occurred when metadata has been converted to TrustedIdPs", e);
return;
}
CompletableFuture.allOf(savedFilesNamesFutures)
.thenRunAsync(
() -> cleanUp(entitiesDescriptorDocument, savedFilesNamesFutures),
executorService)
.whenComplete((result, error) -> currentlyDownloadingFederation.remove(federationId))
.whenComplete((result, error) -> log.info("Prefetched logos of federation {}", federationId));
}
private void cleanUp(EntitiesDescriptorDocument entitiesDescriptorDocument, CompletableFuture>[] savedFilesNamesFutures)
{
Set downloadedFilesName = Arrays.stream(savedFilesNamesFutures)
.filter(future -> !future.isCompletedExceptionally())
.flatMap(this::getFileNamesAfterJobCompletion)
.collect(Collectors.toSet());
cleanUp(entitiesDescriptorDocument.getEntitiesDescriptor().getID(), downloadedFilesName);
}
private Stream getFileNamesAfterJobCompletion(CompletableFuture> completableFuture)
{
try
{
return completableFuture.get().stream();
} catch (InterruptedException | ExecutionException e)
{
throw new IllegalStateException("This shouldn't happen, only completed future should be processed ", e);
}
}
private void cleanUp(String federationId, Set downloadedFilesName)
{
String catalog = LogoFilenameUtils.federationDirName(federationId);
try
{
Path finalDir = Paths.get(workspaceDir, catalog);
Paths.get(workspaceDir, STAGING, catalog).toFile().deleteOnExit();
if(!finalDir.toFile().exists())
return;
removeFilesFromFinalDestinationWhichAreNotReplacedByNewOne(downloadedFilesName, finalDir);
log.debug("Not used logos from federation id {} has been cleaned from {}", federationId, finalDir);
}
catch (IOException e)
{
log.error("Failed while cleaning images from final destination", e);
}
}
private static void removeFilesFromFinalDestinationWhichAreNotReplacedByNewOne(Set savedFilesBasedNames,
Path finalDir) throws IOException
{
try (Stream paths = Files.walk(finalDir))
{
paths.filter(Files::isRegularFile)
.filter(path -> savedFilesBasedNames.stream().noneMatch(name -> path.getFileName().toString().startsWith(name)))
.forEach(path -> path.toFile().delete());
}
}
private Set downloadFiles(Map.Entry entry, String httpsTruststore)
{
return entry.getValue().logoURI
.getMap()
.entrySet().stream()
.map(entry1 ->
{
String federationDirName = LogoFilenameUtils.federationDirName(entry.getValue().federationId);
String logoFileBasename = LogoFilenameUtils.getLogoFileBasename(entry.getKey(), new Locale(entry1.getKey()), defaultLocale);
fetchAndSaveFileOnDisk(federationDirName, logoFileBasename, entry1.getValue(), httpsTruststore);
return logoFileBasename;
}
).collect(Collectors.toSet());
}
private void fetchAndSaveFileOnDisk(String catalog, String name, String logoURI, String httpsTruststore)
{
try
{
URI uri = URI.create(logoURI);
if(uri.getScheme().equals("data"))
saveFileBasedOnDataURI(catalog, name, uri);
else
downloadFile(catalog, name, uri, httpsTruststore);
log.trace("Logo file with uri {} was downloaded to {}", logoURI, name);
} catch (Exception e)
{
String cause = e.getCause() != null ? e.getCause().getMessage() : "-";
if (e.getCause() == null || !knownException(e.getCause()))
log.debug("Details of fetching logo {} error", logoURI, e);
else if (log.isTraceEnabled())
log.trace("Details of fetching logo {} error", logoURI, e);
else
log.debug("Logo file with uri {} cannot be downloaded: {}, cause: {}", logoURI, e.getMessage(), cause);
}
}
private boolean knownException(Throwable exception)
{
return exception instanceof IOException;
}
private void downloadFile(String catalog, String name, URI uri, String httpsTruststore) throws IOException
{
log.trace("Downloading from {}", uri);
RemoteFileData fileData = uriAccessService.readURL(uri, httpsTruststore, connectionTimeout, socketReadTimeout, 0);
String extension = LogoFilenameUtils.getExtensionForRemoteFile(fileData);
saveImageFileAndItsPointer(catalog, name, fileData.getContents(), extension);
}
private void saveFileBasedOnDataURI(String catalog, String name, URI logoURI) throws IOException
{
String logoURIStr = logoURI.toString();
int dataStartIndex = logoURIStr.indexOf(",") + 1;
String data = logoURIStr.substring(dataStartIndex);
byte[] decoded = getDecoder().decode(data);
String extension = LogoFilenameUtils.getExtensionFromDataURI(logoURI);
saveImageFileAndItsPointer(catalog, name, decoded, extension);
}
private void saveImageFileAndItsPointer(String catalog, String name, byte[] decoded, String extension) throws IOException
{
File imageFile = createFile(catalog, name + "." + extension);
Files.write(imageFile.toPath(), decoded);
File pointerFile = createFile(catalog, name);
Files.write(pointerFile.toPath(), extension.getBytes(StandardCharsets.UTF_8));
try
{
FileUtils.moveFile(imageFile, new File(Path.of(workspaceDir, catalog, name + "." + extension).toUri()));
FileUtils.moveFile(pointerFile, new File(Path.of(workspaceDir, catalog, name).toUri()));
}
catch (FileExistsException e)
{
imageFile.delete();
pointerFile.delete();
}
}
private File createFile(String catalog, String name) throws IOException
{
new File(Path.of(workspaceDir, STAGING, catalog).toUri()).mkdirs();
File file = new File(Path.of(workspaceDir, STAGING, catalog, name).toUri());
file.createNewFile();
return file;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy