diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java new file mode 100644 index 00000000..62d833f5 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java @@ -0,0 +1,1887 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.downloader.Downloader; +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; +import org.schabi.newpipe.extractor.utils.Utils; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*; +import static org.schabi.newpipe.extractor.utils.Utils.*; + +/** + * Class to generate DASH manifests from YouTube OTF, progressive and ended/post-live-DVR streams. + * + *
+ * It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages. + *
+ */ +@SuppressWarnings({"ConstantConditions", "unused"}) +public final class YoutubeDashManifestCreator { + + /** + * URL parameter of the first sequence for live, post-live-DVR and OTF streams. + */ + private static final String SQ_0 = "&sq=0"; + + /** + * URL parameter of the first stream request made by official clients. + */ + private static final String RN_0 = "&rn=0"; + + /** + * URL parameter specific to web clients. When this param is added, if a redirection occurs, + * the server will not redirect clients to the redirect URL. Instead, it will provide this URL + * as the response body. + */ + private static final String ALR_YES = "&alr=yes"; + + /** + * The redirect count limit that this class uses, which is the same limit as OkHttp. + */ + private static final int MAXIMUM_REDIRECT_COUNT = 20; + + /** + * A list of durations of segments of an OTF stream. + * + *+ * This list is automatically cleared in the execution of + * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * manifest is converted to a string. + *
+ */ + private static final List+ * This list is automatically cleared in the execution of + * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * manifest is converted to a string. + *
+ */ + private static final List+ * Initialization and index ranges are available to get metadata (the corresponding values + * are returned in the player response). + *
+ */ + PROGRESSIVE, + /** + * YouTube's OTF delivery method which uses a sequence parameter to get segments of + * streams. + * + *+ * The first sequence (which can be fetched with the {@link #SQ_0} param) contains all the + * metadata needed to build the stream source (sidx boxes, segment length, segment count, + * duration, ...) + *
+ *+ * Only used for videos; mostly those with a small amount of views, or ended livestreams + * which have just been re-encoded as normal videos. + *
+ */ + OTF, + /** + * YouTube's delivery method for livestreams which uses a sequence parameter to get + * segments of streams. + * + *+ * Each sequence (which can be fetched with the {@link #SQ_0} param) contains its own + * metadata (sidx boxes, segment length, ...), which make no need of an initialization + * segment. + *
+ *+ * Only used for livestreams (ended or running). + *
+ */ + LIVE + } + + private YoutubeDashManifestCreator() { + } + + /** + * Exception that is thrown when the {@link YoutubeDashManifestCreator} encounters a problem + * while creating a manifest. + */ + public static final class YoutubeDashManifestCreationException extends Exception { + + YoutubeDashManifestCreationException(final String message) { + super(message); + } + + YoutubeDashManifestCreationException(final String message, final Exception e) { + super(message, e); + } + } + + /** + * Create DASH manifests from a YouTube OTF stream. + * + *+ * OTF streams are YouTube-DASH specific streams which work with sequences and without the need + * to get a manifest (even if one is provided, it is not used by official clients). + *
+ *+ * They can be found only on videos; mostly those with a small amount of views, or ended + * livestreams which have just been re-encoded as normal videos. + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param otfBaseStreamingUrl the base URL of the OTF stream, which cannot be null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * cannot be null + * @param durationSecondsFallback the duration of the video, which will be used if the duration + * could not be extracted from the first sequence + * @return the manifest generated into a string + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromOtfStreamingUrl( + @Nonnull String otfBaseStreamingUrl, + @Nonnull final ItagItem itagItem, + final long durationSecondsFallback) + throws YoutubeDashManifestCreationException { + if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) { + return GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl).getSecond(); + } + + final String originalOtfBaseStreamingUrl = otfBaseStreamingUrl; + // Try to avoid redirects when streaming the content by saving the last URL we get + // from video servers. + final Response response = getInitializationResponse(otfBaseStreamingUrl, + itagItem, DeliveryType.OTF); + otfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) + .replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new YoutubeDashManifestCreationException( + "Unable to create the DASH manifest: could not get the initialization URL of the OTF stream: response code " + + responseCode); + } + + final String[] segmentDuration; + + try { + final String[] segmentsAndDurationsResponseSplit = response.responseBody() + // Get the lines with the durations and the following + .split("Segment-Durations-Ms: ")[1] + // Remove the other lines + .split("\n")[0] + // Get all durations and repetitions which are separated by a comma + .split(","); + final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; + if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { + segmentDuration = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); + } else { + segmentDuration = segmentsAndDurationsResponseSplit; + } + } catch (final Exception e) { + throw new YoutubeDashManifestCreationException( + "Unable to generate the DASH manifest: could not get the duration of segments", e); + } + + final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF, + itagItem, durationSecondsFallback); + generatePeriodElement(document); + generateAdaptationSetElement(document, itagItem); + generateRoleElement(document); + generateRepresentationElement(document, itagItem); + if (itagItem.itagType == ItagItem.ItagType.AUDIO) { + generateAudioChannelConfigurationElement(document, itagItem); + } + generateSegmentTemplateElement(document, otfBaseStreamingUrl, DeliveryType.OTF); + generateSegmentTimelineElement(document); + collectSegmentsData(segmentDuration); + generateSegmentElementsForOtfStreams(document); + + SEGMENTS_DURATION.clear(); + DURATION_REPETITIONS.clear(); + + return buildResult(originalOtfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); + } + + /** + * Create DASH manifests from a YouTube post-live-DVR stream/ended livestream. + * + *+ * Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which + * works with sequences and without the need to get a manifest (even if one is provided but not + * used by main clients (and is complete for big ended livestreams because it doesn't return + * the full stream)). + *
+ * + *+ * They can be found only on livestreams which have ended very recently (a few hours, most of + * the time) + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended + * livestream, which cannot be null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * cannot be null + * @param targetDurationSec the target duration of each sequence, in seconds (this + * value is returned with the targetDurationSec field for + * each stream in YouTube player response) + * @param durationSecondsFallback the duration of the ended livestream which will be used + * if the duration could not be extracted from the first + * sequence + * @return the manifest generated into a string + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromPostLiveStreamDvrStreamingUrl( + @Nonnull String postLiveStreamDvrStreamingUrl, + @Nonnull final ItagItem itagItem, + final int targetDurationSec, + final long durationSecondsFallback) + throws YoutubeDashManifestCreationException { + if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) { + return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(postLiveStreamDvrStreamingUrl) + .getSecond(); + } + final String originalPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; + final String streamDuration; + final String segmentCount; + + if (targetDurationSec <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the targetDurationSec value is less than or equal to 0 (" + targetDurationSec + ")"); + } + + try { + // Try to avoid redirects when streaming the content by saving the latest URL we get + // from video servers. + final Response response = getInitializationResponse(postLiveStreamDvrStreamingUrl, + itagItem, DeliveryType.LIVE); + postLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) + .replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code " + + responseCode); + } + + final Map+ * Progressive streams are YouTube DASH streams which work with range requests and without the + * need to get a manifest. + *
+ * + *+ * They can be found on all videos, and for all streams for most of videos which come from a + * YouTube partner, and on videos with a large number of views. + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param progressiveStreamingBaseUrl the base URL of the progressive stream, which cannot be + * null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * cannot be null + * @param durationSecondsFallback the duration of the progressive stream which will be used + * if the duration could not be extracted from the first + * sequence + * @return the manifest generated into a string + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromProgressiveStreamingUrl( + @Nonnull String progressiveStreamingBaseUrl, + @Nonnull final ItagItem itagItem, + final long durationSecondsFallback) throws YoutubeDashManifestCreationException { + if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) { + return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(progressiveStreamingBaseUrl) + .getSecond(); + } + + if (durationSecondsFallback <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the durationSecondsFallback value is less than or equal to 0 (" + durationSecondsFallback + ")"); + } + + final Document document = generateDocumentAndMpdElement(new String[]{}, + DeliveryType.PROGRESSIVE, itagItem, durationSecondsFallback); + generatePeriodElement(document); + generateAdaptationSetElement(document, itagItem); + generateRoleElement(document); + generateRepresentationElement(document, itagItem); + if (itagItem.itagType == ItagItem.ItagType.AUDIO) { + generateAudioChannelConfigurationElement(document, itagItem); + } + generateBaseUrlElement(document, progressiveStreamingBaseUrl); + generateSegmentBaseElement(document, itagItem); + generateInitializationElement(document, itagItem); + + return buildResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); + } + + /** + * Get the "initialization" {@link Response response} of a stream. + * + *+ * This method fetches: + *
+ * This method will follow redirects for web clients, which works in the following way: + *
+ * The duration of OTF streams is not returned into the player response and needs to be + * calculated by adding the duration of each segment. + *
+ * + * @param segmentDuration the segment duration object extracted from the initialization + * sequence of the stream + * @return the duration of the OTF stream + * @throws YoutubeDashManifestCreationException if something goes wrong when parsing the + * {@code segmentDuration} object + */ + private static int getStreamDuration(@Nonnull final String[] segmentDuration) + throws YoutubeDashManifestCreationException { + try { + int streamLengthMs = 0; + for (final String segDuration : segmentDuration) { + final String[] segmentLengthRepeat = segDuration.split("\\(r="); + int segmentRepeatCount = 0; + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + segmentRepeatCount = Integer.parseInt(Utils.removeNonDigitCharacters( + segmentLengthRepeat[1])); + } + final int segmentLength = Integer.parseInt(segmentLengthRepeat[0]); + streamLengthMs += segmentLength + segmentRepeatCount * segmentLength; + } + return streamLengthMs; + } catch (final NumberFormatException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: unable to get the length of the stream", e); + } + } + + /** + * Create a {@link Document} object and generate the {@code
+ * The generated {@code
+ * {@code
+ * If the duration is an integer or a double with less than 3 digits after the decimal point, + * it will be converted into a double with 3 digits after the decimal point. + *
+ * + * @param segmentDuration the segment duration object extracted from the initialization + * sequence of the stream + * @param deliveryType the {@link DeliveryType} of the stream, see the enum for + * possible values + * @param durationSecondsFallback the duration in seconds, extracted from player response, used + * as a fallback + * @return a {@link Document} object which contains a {@code
+ * The {@code
+ * The {@code
+ * This element, with its attributes and values, is: + *
+ *
+ * {@code
+ * The {@code
+ * The {@code
+ * This method is only used when generating DASH manifests of audio streams. + *
+ *
+ * It will produce the following element:
+ *
+ * {@code
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * It generates the following element:
+ *
+ * {@code
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
+ * as the second parameter)
+ *
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * It generates the following element:
+ *
+ * {@code
+ * The {@code
+ * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. + *
+ *
+ * It will produce a {@code
+ *
+ *
+ * The {@code
+ * The {@code
+ * By parsing by the first media sequence, we know how many durations and repetitions there are + * so we just have to loop into {@link #SEGMENTS_DURATION} and {@link #DURATION_REPETITIONS} + * to generate the following element for each duration: + *
+ *
+ * {@code }
+ *
+ * If there is no repetition of the duration between two segments, the {@code r} attribute is + * not added to the {@code S} element. + *
+ *
+ * These elements will be appended as children of the {@code
+ * The {@code
+ * We don't know the exact duration of segments for post-live-DVR streams but an
+ * average instead (which is the {@code targetDurationSec} value), so we can use the following
+ * structure to generate the segment timeline for DASH manifests of ended livestreams:
+ *
+ * {@code }
+ *
+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param otfStreamsCacheLimit the maximum number of OTF streams in the corresponding cache. + */ + public static void setOtfStreamsMaximumSize(final int otfStreamsCacheLimit) { + GENERATED_OTF_MANIFESTS.setMaximumSize(otfStreamsCacheLimit); + } + + /** + * Set the limit of cached post-live-DVR streams. + * + *+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param postLiveDvrStreamsCacheLimit the maximum number of post-live-DVR streams in the + * corresponding cache. + */ + public static void setPostLiveDvrStreamsMaximumSize(final int postLiveDvrStreamsCacheLimit) { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(postLiveDvrStreamsCacheLimit); + } + + /** + * Set the limit of cached progressive streams, if needed. + * + *+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param progressiveCacheLimit the maximum number of progressive streams in the corresponding + * cache. + */ + public static void setProgressiveStreamsMaximumSize(final int progressiveCacheLimit) { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(progressiveCacheLimit); + } + + /** + * Set the limit of cached OTF manifests, cached post-live-DVR manifests and cached progressive + * manifests. + * + *+ * When the caches limit size are reached, oldest manifests will be removed from their + * respective cache. + *
+ * + *+ * For each cache, if its new size set is less than the number of current cached manifests, + * oldest manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param cachesLimit the maximum size of OTF, post-live-DVR and progressive caches + */ + public static void setManifestsCachesMaximumSize(final int cachesLimit) { + GENERATED_OTF_MANIFESTS.setMaximumSize(cachesLimit); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); + } + + /** + * Clear cached OTF manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearOtfCachedManifests() { + GENERATED_OTF_MANIFESTS.clear(); + } + + /** + * Clear cached post-live-DVR streams manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearPostLiveDvrStreamsCachedManifests() { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); + } + + /** + * Clear cached progressive streams manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearProgressiveCachedManifests() { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); + } + + /** + * Clear cached OTF manifests, cached post-live-DVR streams manifests and cached progressive + * manifests in their respective caches. + * + *+ * The limit of the caches size set, if any, will be not unset. + *
+ */ + public static void clearManifestsInCaches() { + GENERATED_OTF_MANIFESTS.clear(); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); + } + + /** + * Reset OTF manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetOtfManifestsCache() { + GENERATED_OTF_MANIFESTS.reset(); + } + + /** + * Reset post-live-DVR manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetPostLiveDvrManifestsCache() { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); + } + + /** + * Reset progressive manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetProgressiveManifestsCache() { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); + } + + /** + * Reset OTF, post-live-DVR and progressive manifests caches. + * + *+ * For each cache, all cached manifests will be removed and the clear factor and the maximum + * size will be set to their default values. + *
+ */ + public static void resetCaches() { + GENERATED_OTF_MANIFESTS.reset(); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java new file mode 100644 index 00000000..8e885f7c --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java @@ -0,0 +1,301 @@ +package org.schabi.newpipe.extractor.utils; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A {@link Serializable serializable} cache class used by the extractor to cache manifests + * generated with extractor's manifests generators. + * + *+ * It relies internally on a {@link ConcurrentHashMap} to allow concurrent access to the cache. + *
+ * + * @param+ * The default value is {@link #DEFAULT_MAXIMUM_SIZE}. + *
+ */ + private int maximumSize = DEFAULT_MAXIMUM_SIZE; + + /** + * The clear factor of the cache, which is a double between {@code 0} and {@code 1} excluded. + * + *+ * The default value is {@link #DEFAULT_CLEAR_FACTOR}. + *
+ */ + private double clearFactor = DEFAULT_CLEAR_FACTOR; + + /** + * Creates a new {@link ManifestCreatorCache}. + */ + public ManifestCreatorCache() { + concurrentHashMap = new ConcurrentHashMap<>(); + } + + /** + * Tests if the specified key is in the cache. + * + * @param key the key to test its presence in the cache + * @return {@code true} if the key is in the cache, {@code false} otherwise. + */ + public boolean containsKey(final K key) { + return concurrentHashMap.containsKey(key); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} if the cache + * contains no mapping for the key. + * + * @param key the key to which getting its value + * @return the value to which the specified key is mapped, or {@code null} + */ + @Nullable + public Pair+ * If the cache limit is reached, oldest elements will be cleared first using the load factor + * and the maximum size. + *
+ * + * @param key the key to put + * @param value the value to associate to the key + * + * @return the previous value associated with the key, or {@code null} if there was no mapping + * for the key (note that a null return can also indicate that the cache previously associated + * {@code null} with the key). + */ + @Nullable + public V put(final K key, final V value) { + if (!concurrentHashMap.containsKey(key) && concurrentHashMap.size() == maximumSize) { + final int newCacheSize = (int) Math.round(maximumSize * clearFactor); + keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); + } + + final Pair+ * The cache will be empty after this method is called. + *
+ */ + public void clear() { + concurrentHashMap.clear(); + } + + /** + * Resets the cache. + * + *+ * The cache will be empty and the clear factor and the maximum size will be reset to their + * default values. + *
+ * + * @see #clear() + * @see #resetClearFactor() + * @see #resetMaximumSize() + */ + public void reset() { + clear(); + resetClearFactor(); + resetMaximumSize(); + } + + /** + * Returns the number of cached manifests in the cache. + * + * @return the number of cached manifests + */ + public int size() { + return concurrentHashMap.size(); + } + + /** + * Gets the maximum size of the cache. + * + * @return the maximum size of the cache + */ + public long getMaximumSize() { + return maximumSize; + } + + /** + * Sets the maximum size of the cache. + * + * If the current cache size is more than the new maximum size, the percentage of one less the + * clear factor of the maximum new size of manifests in the cache will be removed. + * + * @param maximumSize the new maximum size of the cache + * @throws IllegalArgumentException if {@code maximumSize} is less than or equal to 0 + */ + public void setMaximumSize(final int maximumSize) { + if (maximumSize <= 0) { + throw new IllegalArgumentException("Invalid maximum size"); + } + + if (maximumSize < this.maximumSize && !concurrentHashMap.isEmpty()) { + final int newCacheSize = (int) Math.round(maximumSize * clearFactor); + keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); + } + + this.maximumSize = maximumSize; + } + + /** + * Resets the maximum size of the cache to its {@link #DEFAULT_MAXIMUM_SIZE default value}. + */ + public void resetMaximumSize() { + this.maximumSize = DEFAULT_MAXIMUM_SIZE; + } + + /** + * Gets the current clear factor of the cache, used when the cache limit size is reached. + * + * @return the current clear factor of the cache + */ + public double getClearFactor() { + return clearFactor; + } + + /** + * Sets the clear factor of the cache, used when the cache limit size is reached. + * + *+ * The clear factor must be a double between {@code 0} excluded and {@code 1} excluded. + *
+ * + *+ * Note that it will be only used the next time the cache size limit is reached. + *
+ * + * @param clearFactor the new clear factor of the cache + * @throws IllegalArgumentException if the clear factor passed a parameter is invalid + */ + public void setClearFactor(final double clearFactor) { + if (clearFactor <= 0 || clearFactor >= 1) { + throw new IllegalArgumentException("Invalid clear factor"); + } + + this.clearFactor = clearFactor; + } + + /** + * Resets the clear factor to its {@link #DEFAULT_CLEAR_FACTOR default value}. + */ + public void resetClearFactor() { + this.clearFactor = DEFAULT_CLEAR_FACTOR; + } + + /** + * Reveals whether an object is equal to a {@code ManifestCreator} cache existing object. + * + * @param obj the object to compare with the current {@code ManifestCreatorCache} object + * @return whether the object compared is equal to the current {@code ManifestCreatorCache} + * object + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final ManifestCreatorCache, ?> manifestCreatorCache = + (ManifestCreatorCache, ?>) obj; + return maximumSize == manifestCreatorCache.maximumSize + && Double.compare(manifestCreatorCache.clearFactor, clearFactor) == 0 + && concurrentHashMap.equals(manifestCreatorCache.concurrentHashMap); + } + + /** + * Returns a hash code of the current {@code ManifestCreatorCache}, using its + * {@link #maximumSize maximum size}, {@link #clearFactor clear factor} and + * {@link #concurrentHashMap internal concurrent hash map} used as a cache. + * + * @return a hash code of the current {@code ManifestCreatorCache} + */ + @Override + public int hashCode() { + return Objects.hash(maximumSize, clearFactor, concurrentHashMap); + } + + /** + * Returns a string version of the {@link ConcurrentHashMap} used internally as the cache. + * + * @return the string version of the {@link ConcurrentHashMap} used internally as the cache + */ + @Override + public String toString() { + return concurrentHashMap.toString(); + } + + /** + * Keeps only the newest entries in a cache. + * + *+ * This method will first collect the entries to remove by looping through the concurrent hash + * map + *
+ * + * @param newLimit the new limit of the cache + */ + private void keepNewestEntries(final int newLimit) { + final int difference = concurrentHashMap.size() - newLimit; + final ArrayList