AbstractRequestCache.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.impl.cache;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.apache.maven.api.cache.BatchRequestException;
import org.apache.maven.api.cache.MavenExecutionException;
import org.apache.maven.api.cache.RequestCache;
import org.apache.maven.api.cache.RequestResult;
import org.apache.maven.api.services.Request;
import org.apache.maven.api.services.Result;

/**
 * Abstract implementation of the {@link RequestCache} interface, providing common caching mechanisms
 * for executing and caching request results in Maven.
 * <p>
 * This class implements caching strategies for individual and batch requests, ensuring that results
 * are stored and reused where appropriate to optimize performance.
 * </p>
 *
 * @since 4.0.0
 */
public abstract class AbstractRequestCache implements RequestCache {

    /**
     * Executes and optionally caches a single request.
     * <p>
     * The caching behavior is determined by the specific implementation of {@link #doCache(Request, Function)}.
     * If caching is enabled, the result is retrieved from the cache or computed using the supplier function.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param req The request object used as the cache key
     * @param supplier The function that provides the response if not cached
     * @return The cached or computed response
     */
    @Override
    @SuppressWarnings("all")
    public <REQ extends Request<?>, REP extends Result<REQ>> REP request(REQ req, Function<REQ, REP> supplier) {
        CachingSupplier<REQ, REP> cs = doCache(req, supplier);
        return cs.apply(req);
    }

    /**
     * Executes and optionally caches a batch of requests.
     * <p>
     * This method processes a list of requests, utilizing caching where applicable and executing
     * only the non-cached requests using the provided supplier function.
     * </p>
     * <p>
     * If any request in the batch fails, a {@link BatchRequestException} is thrown, containing
     * details of all failed requests.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param reqs List of requests to process
     * @param supplier Function to execute the batch of requests
     * @return List of results corresponding to the input requests
     * @throws BatchRequestException if any request in the batch fails
     */
    @Override
    @SuppressWarnings("unchecked")
    public <REQ extends Request<?>, REP extends Result<REQ>> List<REP> requests(
            List<REQ> reqs, Function<List<REQ>, List<REP>> supplier) {
        final Map<REQ, Object> nonCachedResults = new HashMap<>();
        List<RequestResult<REQ, REP>> allResults = new ArrayList<>(reqs.size());

        Function<REQ, REP> individualSupplier = req -> {
            synchronized (nonCachedResults) {
                while (!nonCachedResults.containsKey(req)) {
                    try {
                        nonCachedResults.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException(e);
                    }
                }
                Object val = nonCachedResults.get(req);
                if (val instanceof CachingSupplier.AltRes altRes) {
                    uncheckedThrow(altRes.throwable);
                }
                return (REP) val;
            }
        };

        List<CachingSupplier<REQ, REP>> suppliers = new ArrayList<>(reqs.size());
        List<REQ> nonCached = new ArrayList<>();
        for (REQ req : reqs) {
            CachingSupplier<REQ, REP> cs = doCache(req, individualSupplier);
            suppliers.add(cs);
            if (cs.getValue() == null) {
                nonCached.add(req);
            }
        }

        if (!nonCached.isEmpty()) {
            synchronized (nonCachedResults) {
                try {
                    List<REP> reps = supplier.apply(nonCached);
                    for (int i = 0; i < reps.size(); i++) {
                        nonCachedResults.put(nonCached.get(i), reps.get(i));
                    }
                } catch (MavenExecutionException e) {
                    // If batch request fails, mark all non-cached requests as failed
                    for (REQ req : nonCached) {
                        nonCachedResults.put(
                                req, new CachingSupplier.AltRes(e.getCause())); // Mark as processed but failed
                    }
                } finally {
                    nonCachedResults.notifyAll();
                }
            }
        }

        // Collect results in original order
        boolean hasFailures = false;
        for (int i = 0; i < reqs.size(); i++) {
            REQ req = reqs.get(i);
            CachingSupplier<REQ, REP> cs = suppliers.get(i);
            try {
                REP value = cs.apply(req);
                allResults.add(new RequestResult<>(req, value, null));
            } catch (Throwable t) {
                hasFailures = true;
                allResults.add(new RequestResult<>(req, null, t));
            }
        }

        if (hasFailures) {
            BatchRequestException exception = new BatchRequestException("One or more requests failed", allResults);
            // Add all individual exceptions as suppressed exceptions to preserve stack traces
            for (RequestResult<REQ, REP> result : allResults) {
                if (result.error() != null) {
                    exception.addSuppressed(result.error());
                }
            }
            throw exception;
        }

        return allResults.stream().map(RequestResult::result).toList();
    }

    /**
     * Abstract method to be implemented by subclasses to handle caching logic.
     * <p>
     * This method is responsible for determining whether a request result should be cached,
     * retrieving it from cache if available, or executing the supplier function if necessary.
     * </p>
     *
     * @param <REQ> The request type
     * @param <REP> The response type
     * @param req The request object
     * @param supplier The function that provides the response
     * @return A caching supplier that handles caching logic for the request
     */
    protected abstract <REQ extends Request<?>, REP extends Result<REQ>> CachingSupplier<REQ, REP> doCache(
            REQ req, Function<REQ, REP> supplier);

    @SuppressWarnings("unchecked")
    protected static <T extends Throwable> void uncheckedThrow(Throwable t) throws T {
        throw (T) t; // rely on vacuous cast
    }
}