org.artifactory.request.RequestResponseHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.request.RequestResponseHelper.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.request;

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.artifactory.api.context.ContextHelper;
import org.artifactory.api.repo.RepositoryService;
import org.artifactory.api.request.ArtifactoryResponse;
import org.artifactory.api.rest.constant.ArtifactRestConstants;
import org.artifactory.descriptor.repo.RealRepoDescriptor;
import org.artifactory.descriptor.repo.RepoDescriptor;
import org.artifactory.fs.RepoResource;
import org.artifactory.mime.NamingUtils;
import org.artifactory.repo.RepoPath;
import org.artifactory.repo.RepoPathFactory;
import org.artifactory.request.range.RangeAwareContext;
import org.artifactory.resource.RepoResourceInfo;
import org.artifactory.resource.ResourceStreamHandle;
import org.artifactory.resource.ZipEntryResource;
import org.artifactory.security.AccessLogger;
import org.artifactory.traffic.TrafficService;
import org.artifactory.traffic.entry.DownloadEntry;
import org.artifactory.util.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import static org.artifactory.api.rest.constant.ArtifactRestConstants.MT_ITEM_PROPERTIES;
import static org.artifactory.request.range.ResponseWithRangeSupportHelper.createRangeAwareContext;

/**
 * @author yoavl
 */
public final class RequestResponseHelper {
    private static final Logger log = LoggerFactory.getLogger(RequestResponseHelper.class);

    private TrafficService trafficService;

    public RequestResponseHelper(TrafficService service) {
        trafficService = service;
    }

    private static void addDebugLog(long actualLength, long resLenght, RepoPath repoPath) {
        if (log.isDebugEnabled()) {
            log.debug("Sending back body response for '{}'. Original resource size: {}, actual size: {}.", repoPath,
                    resLenght, actualLength);
        }
    }

    public void sendBodyResponse(ArtifactoryResponse response, RepoResource res, ResourceStreamHandle handle,
            InternalRequestContext requestContext) throws IOException {
        // Try to get range headers
        String rangesString = tryToGetRangeHeaderInsensitive(HttpHeaders.RANGE, requestContext.getRequest());
        String ifRangesString = tryToGetRangeHeaderInsensitive(HttpHeaders.IF_RANGE, requestContext.getRequest());
        // Get mimetype
        String mimeType = res.getMimeType();
        InputStream inputStream = handle.getInputStream();
        // Get Actual file actualLength (Note that it might not be equal to the res.getSize)
        long actualLength = handle.getSize();
        AccessLogger.downloaded(res.getRepoPath());
        addDebugLog(actualLength, res.getSize(), res.getRepoPath());
        // Ensure valid content length
        actualLength = actualLength > 0 ? actualLength : res.getSize();
        // Get artifact last modified date
        long lastModified = res.getLastModified();
        // Get artifact sha1
        String sha1 = res.getInfo().getSha1();
        // Create range aware data for the response
        RangeAwareContext context = createRangeAwareContext(inputStream, actualLength, rangesString, ifRangesString,
                mimeType, lastModified, sha1);
        // If request range not satisfiable update response status end return
        if (context.getStatus() == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
            response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
            response.setStatus(context.getStatus());
            return;
        }
        // Update response with repo resource info
        updateResponseFromRepoResource(response, res, context.getEtagExtension());
        // update content length with range aware content length
        response.setContentLength(context.getContentLength());
        // update content type with range aware content type
        response.setContentType(context.getContentType());
        // update headers with range aware headers
        if (context.getContentRange() != null) {
            response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
        }
        // Set response status
        if (context.getStatus() > 0) {
            response.setStatus(context.getStatus());
        }
        // Get range aware input stream
        inputStream = context.getInputStream();
        // Get current time for logs
        long start = System.currentTimeMillis();
        // Send range aware input stream
        response.sendStream(inputStream);
        fireDownloadTrafficEvent(response, res.getRepoPath(), actualLength, start);
    }

    private String tryToGetRangeHeaderInsensitive(String name, Request request) {
        String header = request.getHeader(name);
        if (StringUtils.isBlank(header)) {
            header = request.getHeader(name.toLowerCase());
        }
        return header;
    }

    public void sendBodyResponse(ArtifactoryResponse response, RepoPath repoPath, String content, Request request)
            throws IOException {
        // Make sure that the response is not empty
        if (content == null) {
            RuntimeException exception = new RuntimeException("Cannot send null response");
            response.sendInternalError(exception, log);
            throw exception;
        }
        byte[] bytes = content.getBytes("utf-8");
        try (InputStream is = new ByteArrayInputStream(bytes)) {
            String path = repoPath.getPath();
            String mimeType = NamingUtils.getMimeTypeByPathAsString(path);
            response.setContentType(mimeType);
            int bodySize = bytes.length;
            response.setContentLength(bodySize);
            response.setLastModified(System.currentTimeMillis());
            AccessLogger.downloaded(repoPath);
            // Try to get range header
            String rangesString = tryToGetRangeHeaderInsensitive(HttpHeaders.RANGE, request);
            // Create range aware data for the response
            // If-Range is not supported
            RangeAwareContext context = createRangeAwareContext(is, bodySize, rangesString, null, mimeType, -1,
                    null);
            // If request range not satisfiable update response status end return
            if (context.getStatus() == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
                response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
                response.setStatus(context.getStatus());
                return;
            }
            // update content length with range aware content length
            response.setContentLength(context.getContentLength());
            // update content type with range aware content type
            response.setContentType(context.getContentType());
            // update headers with range aware headers
            if (context.getContentRange() != null) {
                response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
            }
            // Get range aware input stream
            InputStream inputStream = context.getInputStream();
            // Set response status
            if (context.getStatus() > 0) {
                response.setStatus(context.getStatus());
            }
            // Get current time for logs
            long start = System.currentTimeMillis();
            response.sendStream(inputStream);
            fireDownloadTrafficEvent(response, repoPath, bodySize, start);
        }
    }

    private void fireDownloadTrafficEvent(ArtifactoryResponse response, RepoPath repoPath, long size, long start) {
        if (!(response instanceof InternalArtifactoryResponse)) {
            String remoteAddress = HttpUtils.getRemoteClientAddress();
            DownloadEntry downloadEntry = new DownloadEntry(repoPath.getId(), size,
                    System.currentTimeMillis() - start, remoteAddress);
            trafficService.handleTrafficEntry(downloadEntry);
        }
    }

    public void sendHeadResponse(ArtifactoryResponse response, RepoResource res) {
        log.debug("{}: Sending HEAD meta-information", res.getRepoPath());
        if (!isContentLengthSet(response)) {
            response.setContentLength(res.getSize());
        }
        updateResponseFromRepoResource(response, res, "");
        response.setContentType(res.getMimeType());
        response.sendSuccess();
    }

    public void sendNotModifiedResponse(ArtifactoryResponse response, RepoResource res) throws IOException {
        log.debug("{}: Sending NOT-MODIFIED response", res.toString());
        response.setContentLength(0);
        updateResponseFromRepoResource(response, res, "");
        response.setContentType(res.getMimeType());
        response.setStatus(HttpStatus.SC_NOT_MODIFIED);
    }

    public void updateResponseForProperties(ArtifactoryResponse response, RepoResource res, String content,
            MediaType mediaType, InternalRequestContext requestContext) throws IOException {
        RepoPath propsDownloadRepoPath;
        String contentType;
        if (mediaType.equals(MediaType.APPLICATION_XML)) {
            propsDownloadRepoPath = RepoPathFactory.create(res.getRepoPath().getRepoKey(),
                    res.getRepoPath().getPath() + "?" + ArtifactRestConstants.PROPERTIES_XML_PARAM);
            contentType = mediaType.getType();
        } else if (mediaType.equals(MediaType.APPLICATION_JSON)) {
            propsDownloadRepoPath = RepoPathFactory.create(res.getRepoPath().getRepoKey(),
                    res.getRepoPath().getPath() + "?" + ArtifactRestConstants.PROPERTIES_PARAM);
            contentType = MT_ITEM_PROPERTIES;
        } else {
            response.sendError(HttpStatus.SC_BAD_REQUEST, "Media Type " + mediaType + " not supported!", log);
            return;
        }
        // props generated xml and json always browsable
        setBasicHeaders(response, res, false, "");
        noCache(response);
        byte[] bytes = content.getBytes("utf-8");
        try (InputStream is = new ByteArrayInputStream(bytes)) {
            int bodySize = bytes.length;
            response.setContentLength(bodySize);
            AccessLogger.downloaded(propsDownloadRepoPath);
            // Try to get range header
            String rangesString = tryToGetRangeHeaderInsensitive(HttpHeaders.RANGE, requestContext.getRequest());
            String ifRangesString = tryToGetRangeHeaderInsensitive(HttpHeaders.IF_RANGE,
                    requestContext.getRequest());
            // Get artifact last modified date
            long lastModified = res.getLastModified();
            // Get artifact sha1
            String sha1 = res.getInfo().getSha1();
            // Create range aware data for the response
            RangeAwareContext context = createRangeAwareContext(is, bodySize, rangesString, ifRangesString,
                    contentType, lastModified, sha1);
            // If request range not satisfiable update response status end return
            if (context.getStatus() == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) {
                response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
                response.setStatus(context.getStatus());
                return;
            }
            // update content length with range aware content length
            response.setContentLength(context.getContentLength());
            // update content type with range aware content type
            response.setContentType(context.getContentType());
            // update headers with range aware headers
            if (context.getContentRange() != null) {
                response.setHeader(HttpHeaders.CONTENT_RANGE, context.getContentRange());
            }
            // Set response status
            if (context.getStatus() > 0) {
                response.setStatus(context.getStatus());
            }
            // Get range aware input stream
            InputStream inputStream = context.getInputStream();
            // Get current time for logs
            long start = System.currentTimeMillis();
            // Send stream
            response.sendStream(inputStream);
            // Fire Download traffic event
            fireDownloadTrafficEvent(response, propsDownloadRepoPath, bodySize, start);
        }
    }

    private void updateResponseFromRepoResource(ArtifactoryResponse response, RepoResource res,
            String etagExtension) {
        setBasicHeaders(response, res, contentBrowsingDisabled(res), etagExtension);
        if (res.isExpirable()) {
            noCache(response);
        }
    }

    private boolean isContentLengthSet(ArtifactoryResponse response) {
        return response.getContentLength() != -1;
    }

    private void setBasicHeaders(ArtifactoryResponse response, RepoResource res, boolean contentBrowsingDisabled,
            String etagExtension) {
        response.setLastModified(res.getLastModified());
        RepoResourceInfo info = res.getInfo();
        // set the sha1 as the eTag and the sha1 header
        String sha1 = info.getSha1();
        String etag = sha1 != null ? sha1 + etagExtension : null;
        response.setEtag(etag);
        response.setSha1(sha1);
        response.setRangeSupport("bytes");
        // set the md5 header
        String md5 = info.getMd5();
        response.setMd5(md5);
        if (response instanceof ArtifactoryResponseBase) {
            String fileName = info.getName();
            if (!isNotZipResource(res)) {
                // The filename is the zip entry inside the zip
                ZipEntryResource zipEntryResource = (ZipEntryResource) res;
                fileName = zipEntryResource.getEntryPath();
            }
            ((ArtifactoryResponseBase) response).setFilename(fileName);

            // content disposition is not set only for archive resources when archived browsing is enabled
            if (contentBrowsingDisabled) {
                ((ArtifactoryResponseBase) response).setContentDispositionAttachment(fileName);
            }
        }
    }

    private void noCache(ArtifactoryResponse response) {
        response.setHeader("Cache-Control", "no-store");
    }

    private boolean isNotZipResource(RepoResource res) {
        return !(res instanceof ZipEntryResource);
    }

    private boolean contentBrowsingDisabled(RepoResource res) {
        boolean result = true;
        RepositoryService repositoryService = ContextHelper.get().getRepositoryService();
        String repoKey = res.getResponseRepoPath().getRepoKey();
        RepoDescriptor repoDescriptor = repositoryService.repoDescriptorByKey(repoKey);
        if (repoDescriptor != null) {
            if (repoDescriptor instanceof RealRepoDescriptor) {
                result = !((RealRepoDescriptor) repoDescriptor).isArchiveBrowsingEnabled();
            }
        }

        // We return true by default if we couldn't get the flag from the descriptor
        return result;
    }
}