25300c04创建于 2024年11月4日历史提交
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights reserved.
 */

/**
 * @file
 * The file declares the CallServerInterceptor class.
 */

package httpclient4cj

/**
 * The class is CacheInterceptor inherited from Interceptor
 * @author luoyukai4
 * @since 0.35.3
 */
public class CacheInterceptor <: Interceptor {
    let cache: Option<Cache>

    public init(cache: Option<Cache>) {
        this.cache = cache
    }

    public func intercept(chain: Chain): Response {
        let cacheCandidate: ?Response = cache?.get(chain.getRequest()) ?? None
        let strategy: CacheStrategy = CacheStrategyFactory(chain.getRequest(), cacheCandidate).get()
        let networkRequest: Option<Request> = strategy.networkRequest
        let cacheResponse: Option<Response> = strategy.cacheResponse

        if (has(cache)) {
            cache.getOrThrow().trackResponse(strategy)
        }

        if (has(cacheCandidate) && !has(cacheResponse)) {
            Util.closeQuietly(cacheCandidate.getOrThrow().getBody())
        }

        if (!has(networkRequest) && !has(cacheResponse)) {
            return Response.builder().request(chain.getRequest()).protocol(Protocol.HTTP_1_1).code(504).message(
                "Unsatisfiable Request (only-if-cached)").body(ResponseBody("", 0, OkBuffer(ByteBuffer()))).
                sentRequestAtMillis(-1).receivedResponseAtMillis(DateTime.now().toUnixTimeStamp().toMilliseconds()).
                build()
        }

        if (!has(networkRequest)) {
            return cacheResponse.getOrThrow().newBuilder().cacheResponse(stripBody(cacheResponse)).build()
        }

        let realNetworkRequest = networkRequest.getOrThrow()
        var networkResponse = Response.builder().build()

        try {
            networkResponse = chain.proceed(realNetworkRequest)
        } finally {
            if (!has(networkResponse) && has(cacheCandidate)) {
                Util.closeQuietly(cacheCandidate.getOrThrow().getBody())
            }
        }

        if (has(cacheResponse)) {
            let cacheRes = cacheResponse.getOrThrow()
            if (networkResponse.getCode() == HttpStatusCode.STATUS_NOT_MODIFIED) {
                let response: Response = cacheRes.newBuilder().header(
                    combine(cacheRes.getHeaders(), networkResponse.getHeaders())).sentRequestAtMillis(
                    networkResponse.getSentRequestAtMillis()).receivedResponseAtMillis(
                    networkResponse.getReceivedResponseAtMillis()).cacheResponse(stripBody(cacheResponse)).
                    networkResponse(stripBody(networkResponse)).build()
                networkResponse.getBody().getOrThrow().close()
                cache.getOrThrow().trackConditionalCacheHit()
                cache.getOrThrow().update(cacheRes, response)
                return response
            } else {
                Util.closeQuietly(cacheRes.getBody())
            }
        }

        let response: Response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(
            stripBody(networkResponse)).build()

        if (has(cache)) {
            if (hasBody(response) && isCacheable(response, realNetworkRequest)) {
                let cacheRequest: Option<CacheRequest> = cache.getOrThrow().put(response)
                return cacheWritingResponse(cacheRequest, response)
            }

            if (invalidatesCache(realNetworkRequest.getMethod())) {
                try {
                    cache.getOrThrow().remove(realNetworkRequest)
                } catch (ignored: FSException | EOFException) {
                    // The cache cannot be written
                }
            }
        }

        return response
    }

    func cacheWritingResponse(cacheRequest: Option<CacheRequest>, response: Response) {
        let body: CacheRequest = match (cacheRequest) {
            case None => return response
            case Some(v) => v
        }

        let source = response.getBody().getOrThrow().getSource()
        let contentType = response.getHeader("Content-Type", "")
        let contentLength = response.getBody().getOrThrow().getContentLength()
        let responseBody = ResponseBody(contentType, contentLength, CacheWritingSource(source, body))
        return response.newBuilder().body(responseBody).build()
    }

    /** Combines cached headers with a network headers as defined by RFC 7234, 4.3.4. */
    private func combine(cachedHeaders: Header, networkHeaders: Header) {
        let result = Header()

        for ((k, v) in cachedHeaders) {
            let fieldName = k
            let value = v[0]
            if (equalsIgnoreCase("Warning", fieldName) && value.startsWith("1")) {
                continue
                // Drop 100-level freshness warnings.
            }

            let func1 = { => result.add(fieldName, value.trim())}
            if (isContentSpecificHeader(fieldName)) {
                func1()
            } else if (!isEndToEnd(fieldName)) {
                func1()
            } else if (has(networkHeaders.get(fieldName))) {
                func1()
            }
        }

        for ((k, v) in networkHeaders) {
            let func2 = { => result.add(k, v[0].trim())}
            if (isContentSpecificHeader(k)) {
                func2()
            } else if (!isEndToEnd(k)) {
                func2()
            }
        }

        return result
    }

    /* Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616, 13.5.1.*/
    static func isEndToEnd(fieldName: String): Bool {
        return !equalsIgnoreCase("Connection", fieldName) && !equalsIgnoreCase("Keep-Alive", fieldName) &&
            !equalsIgnoreCase("Proxy-Authenticate", fieldName) && !equalsIgnoreCase("Proxy-Authorization", fieldName) &&
            !equalsIgnoreCase("TE", fieldName) && !equalsIgnoreCase("Trailers", fieldName) &&
            !equalsIgnoreCase("Transfer-Encoding", fieldName) && !equalsIgnoreCase("Upgrade", fieldName)
    }

    /* Returns true if fieldName is content specific and therefore should always be used */
    static func isContentSpecificHeader(fieldName: String): Bool {
        return equalsIgnoreCase("Content-Length", fieldName) || equalsIgnoreCase("Content-Encoding", fieldName) ||
            equalsIgnoreCase("Content-Type", fieldName)
    }

    private func stripBody(response: Option<Response>): Option<Response> {
        match (response) {
            case Some(v) => if (has(v.getBody())) {
                return v.newBuilder().body(Option<ResponseBody>.None).build()
            } else {
                return v
            }

            case None => return Option<Response>.None
        }
    }
}

class CacheStrategyFactory {
    let request: Request
    let cacheResponse: Option<Response>
    var servedDate: Option<DateTime> = Option<DateTime>.None
    var servedDateString: String = ""
    var lastModified: Option<DateTime> = Option<DateTime>.None
    var lastModifiedString: String = ""
    var expires: Option<DateTime> = Option<DateTime>.None
    var etag: String = ""
    var ageSeconds: Int64 = 0
    let nowMillis = DateTime.now().toUnixTimeStamp().toMilliseconds()
    var sentRequestMillis: Int64 = 0
    var receivedResponseMillis: Int64 = 0

    public init(request: Request, cacheResponse: Option<Response>) {
        this.request = request
        this.cacheResponse = cacheResponse

        if (has(cacheResponse)) {
            let cacheRes = cacheResponse.getOrThrow()
            this.sentRequestMillis = cacheRes.getSentRequestAtMillis()
            this.receivedResponseMillis = cacheRes.getReceivedResponseAtMillis()
            let headers: Header = cacheRes.getHeaders()
            for ((k, v) in headers) {
                let value = v[0]
                if (equalsIgnoreCase("Date", k)) {
                    servedDate = CookieUtil.parseExpires(value, 0, value.size)
                } else if (equalsIgnoreCase("Expires", k)) {
                    expires = CookieUtil.parseExpires(value, 0, value.size)
                } else if (equalsIgnoreCase("Last-Modified", k)) {
                    lastModified = CookieUtil.parseExpires(value, 0, value.size)
                    lastModifiedString = value
                } else if (equalsIgnoreCase("ETag", k)) {
                    etag = value
                } else if (equalsIgnoreCase("Age", k)) {
                    ageSeconds = Util.parseSeconds(value, -1)
                }
            }
        }
    }

    public func get(): CacheStrategy {
        let candidate: CacheStrategy = getCandidate()

        if (has(candidate.networkRequest)) {
            if (request.getCacheControl().onlyIfCached()) {
                return CacheStrategy(Option<Request>.None, Option<Response>.None)
            }
        }

        return candidate
    }

    private func hasConditions(request: Request): Bool {
        return request.getHeader("If-Modified-Since", "") != "" || request.getHeader("If-None-Match", "") != ""
    }

    private func getCandidate() {
        //没有从缓存目录解析到response, 直接使用网络请求
        if (!has(cacheResponse)) {
            return CacheStrategy(request, Option<Response>.None)
        }

        let cacheResp = cacheResponse.getOrThrow()

        //对缓存的response的状态码进行校验,一些特殊的状态码不论怎样都走网络请求
        if (!isCacheable(cacheResp, request)) {
            return CacheStrategy(request, Option<Response>.None)
        }

        let requestCaching: CacheControl = request.getCacheControl()

        // 如果请求的Cache-Control中指定了no-cache,则使用网络请求,或者请求头中配置缓存过期时间则使用网络请求
        if (requestCaching.noCache() || hasConditions(request)) {
            return CacheStrategy(request, Option<Response>.None)
        }

        let responseCaching: CacheControl = cacheResp.getCacheControl()

        // 计算当前缓存的response的存活时间以及缓存应当被刷新的时间
        let ageMillis = cacheResponseAge()
        var freshMillis = computeFreshnessLifetime()

        if (requestCaching.maxAgeSeconds() != -1) {
            freshMillis = min(freshMillis, requestCaching.maxAgeSeconds() * 1000)
        }

        var minFreshMillis = 0
        if (requestCaching.minFreshSeconds() != -1) {
            minFreshMillis = requestCaching.minFreshSeconds()
        }

        var maxStaleMillis = 0

        if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
            maxStaleMillis = requestCaching.maxStaleSeconds() * 1000
        }

        // 对未超过时限的缓存,直接采用缓存数据策略
        if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
            let builder: ResponseBuilder = cacheResp.newBuilder()
            builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
            var oneDayMillis = 24 * 60 * 60 * 1000
            let falg = isFreshnessLifetimeHeuristic()
            if (falg && ageMillis > oneDayMillis) {
                builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
            }

            return CacheStrategy(Option<Request>.None, builder.build())
        }

        //对If-None-Match、If-Modified-Since等Header进行处理
        var conditionName: String
        var conditionValue: String

        //优限度为etag>lastModified>servedDate
        if (etag != "") {
            conditionName = "If-None-Match"
            conditionValue = etag
        } else if (has(lastModified)) {
            conditionName = "If-Modified-Since"
            conditionValue = lastModifiedString
        } else if (has(servedDate)) {
            conditionName = "If-Modified-Since"
            conditionValue = servedDateString
        } else {
            //response未配置缓存过期时间则使用网络请求
            return CacheStrategy(request, Option<Response>.None)
        }

        //存在的话,则在请求中添加该请求头并合本地cacheResponse创建缓存策略
        let conditionalRequestHeaders = request.getHeaders().clone()
        conditionalRequestHeaders.add(conditionName, conditionValue.trim())
        var conditionalRequest = request.newBuilder().header(conditionalRequestHeaders).build()
        return CacheStrategy(conditionalRequest, cacheResponse)
    }

    private func isFreshnessLifetimeHeuristic(): Bool {
        return cacheResponse.getOrThrow().getCacheControl().maxAgeSeconds() == -1 && !has(expires)
    }

    private func computeFreshnessLifetime(): Int64 {
        let responseCaching: CacheControl = cacheResponse.getOrThrow().getCacheControl()

        if (responseCaching.maxAgeSeconds() != -1) {
            return responseCaching.maxAgeSeconds() * 1000
        } else if (has(expires)) {
            let servedMillis = match (servedDate) {
                case Some(v) => v.toUnixTimeStamp().toMilliseconds()
                case None => receivedResponseMillis
            }
            var delta = expires.getOrThrow().toUnixTimeStamp().toMilliseconds() - servedMillis

            if (delta < 0) {
                delta = 0
            }

            return delta
        } else if (has(lastModified) && cacheResponse.getOrThrow().getRequest().getUrl().rawQuery == None) {
            let servedMillis = match (servedDate) {
                case Some(v) => v.toUnixTimeStamp().toMilliseconds()
                case None => sentRequestMillis
            }
            var delta = servedMillis - lastModified.getOrThrow().toUnixTimeStamp().toMilliseconds()

            if (delta < 0) {
                delta = 0
            }

            return delta / 10
        }
        return 0
    }

    private func cacheResponseAge(): Int64 {
        let apparentReceivedAge = match (servedDate) {
            case Some(v) => max(0, receivedResponseMillis - v.toUnixTimeStamp().toMilliseconds())
            case None => 0
        }

        let receivedAge = if (ageSeconds != -1) {
            max(apparentReceivedAge, ageSeconds * 1000)
        } else {
            apparentReceivedAge
        }

        let responseDuration = receivedResponseMillis - sentRequestMillis
        let residentDuration = nowMillis - receivedResponseMillis

        return receivedAge + responseDuration + residentDuration
    }
}

func isCacheable(response: Response, request: Request) {
    match (response.getCode()) {
        //以下code除非备header禁止,否则会缓存
        case 200 | 203 | 204 | 300 | 301 | 308 | 404 | 405 | 410 | 414 | 501 => ()

        //以下code只有在特定header下能缓存
        // s-maxage is not checked because httpclient4cj is a private cache that should ignore s-maxage.
        case 302 | 307 =>
            let flag1 = response.getHeader("Expires", "") != ""
            let flag2 = response.getCacheControl().maxAgeSeconds() != -1
            let flag3 = response.getCacheControl().isPublic()
            let flag4 = response.getCacheControl().isPrivate()
            let flag = flag1 || flag2 || flag3 || flag4
            if (!flag) {
                return false
            }

        //其他所有code都不缓存
        case _ => return false
    }
    if (!response.getCacheControl().noStore()) {
        if (!request.getCacheControl().noStore()) {
            return true
        }
    }
    return false
}

class CacheStrategy {
    public let networkRequest: Option<Request>
    public let cacheResponse: Option<Response>

    init(networkRequest: Option<Request>, cacheResponse: Option<Response>) {
        this.networkRequest = networkRequest
        this.cacheResponse = cacheResponse
    }
}