Package org.sonatype.nexus.proxy.repository

Source Code of org.sonatype.nexus.proxy.repository.AbstractProxyRepository

/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2007-2014 Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.proxy.repository;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.inject.Inject;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;

import org.sonatype.configuration.ConfigurationException;
import org.sonatype.nexus.configuration.model.CRemoteStorage;
import org.sonatype.nexus.proxy.IllegalOperationException;
import org.sonatype.nexus.proxy.ItemNotFoundException;
import org.sonatype.nexus.proxy.LocalStorageEOFException;
import org.sonatype.nexus.proxy.LocalStorageException;
import org.sonatype.nexus.proxy.RemoteAccessDeniedException;
import org.sonatype.nexus.proxy.RemoteAccessException;
import org.sonatype.nexus.proxy.RemoteStorageException;
import org.sonatype.nexus.proxy.RemoteStorageTransportException;
import org.sonatype.nexus.proxy.ResourceStoreRequest;
import org.sonatype.nexus.proxy.StorageException;
import org.sonatype.nexus.proxy.access.Action;
import org.sonatype.nexus.proxy.events.NexusStoppedEvent;
import org.sonatype.nexus.proxy.events.RepositoryConfigurationUpdatedEvent;
import org.sonatype.nexus.proxy.events.RepositoryEventEvictUnusedItems;
import org.sonatype.nexus.proxy.events.RepositoryEventExpireProxyCaches;
import org.sonatype.nexus.proxy.events.RepositoryEventProxyModeChanged;
import org.sonatype.nexus.proxy.events.RepositoryEventProxyModeSet;
import org.sonatype.nexus.proxy.events.RepositoryItemEventCacheCreate;
import org.sonatype.nexus.proxy.events.RepositoryItemEventCacheUpdate;
import org.sonatype.nexus.proxy.events.RepositoryItemValidationEvent;
import org.sonatype.nexus.proxy.item.AbstractStorageItem;
import org.sonatype.nexus.proxy.item.RepositoryItemUid;
import org.sonatype.nexus.proxy.item.RepositoryItemUidLock;
import org.sonatype.nexus.proxy.item.StorageCollectionItem;
import org.sonatype.nexus.proxy.item.StorageItem;
import org.sonatype.nexus.proxy.repository.EvictUnusedItemsWalkerProcessor.EvictUnusedItemsWalkerFilter;
import org.sonatype.nexus.proxy.repository.threads.ThreadPoolManager;
import org.sonatype.nexus.proxy.storage.UnsupportedStorageOperationException;
import org.sonatype.nexus.proxy.storage.remote.AbstractHTTPRemoteRepositoryStorage;
import org.sonatype.nexus.proxy.storage.remote.DefaultRemoteStorageContext;
import org.sonatype.nexus.proxy.storage.remote.RemoteRepositoryStorage;
import org.sonatype.nexus.proxy.storage.remote.RemoteStorageContext;
import org.sonatype.nexus.proxy.utils.RepositoryStringUtils;
import org.sonatype.nexus.proxy.walker.DefaultWalkerContext;
import org.sonatype.nexus.proxy.walker.WalkerException;
import org.sonatype.nexus.proxy.walker.WalkerFilter;
import org.sonatype.nexus.util.SystemPropertiesHelper;
import org.sonatype.nexus.util.sequence.ConstantNumberSequence;
import org.sonatype.nexus.util.sequence.FibonacciNumberSequence;
import org.sonatype.nexus.util.sequence.NumberSequence;

import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import org.codehaus.plexus.util.ExceptionUtils;
import org.codehaus.plexus.util.StringUtils;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.sonatype.nexus.proxy.ItemNotFoundException.reasonFor;

/**
* Adds the proxying capability to a simple repository. The proxying will happen only if reposiory has remote storage!
* So, this implementation is used in both "simple" repository cases: hosted and proxy, but in 1st case there is no
* remote storage.
*
* @author cstamas
*/
public abstract class AbstractProxyRepository
    extends AbstractRepository
    implements ProxyRepository
{

  /**
   * Default time to do NOT check an already known remote status: 5 mins.
   */
  private static final long DEFAULT_REMOTE_STATUS_RETAIN_TIME = 5L * 60L * 1000L;

  /**
   * The time while we do NOT check an already known remote status
   */
  private static final long REMOTE_STATUS_RETAIN_TIME = SystemPropertiesHelper.getLong(
      "plexus.autoblock.remote.status.retain.time", DEFAULT_REMOTE_STATUS_RETAIN_TIME);

  /**
   * The maximum amount of time to have a repository in AUTOBlock status: 60 minutes (1hr). This value is system
   * default, is used only as limiting point. When repository steps here, it will be checked for remote status hourly
   * only (unless forced by user).
   */
  private static final long AUTO_BLOCK_STATUS_MAX_RETAIN_TIME = 60L * 60L * 1000L;

  // == injected

  private ThreadPoolManager poolManager;

  // == set by this

  /**
   * The remote status checker thread, used in Proxies for handling autoBlocking. Not to go into Pool above, is
   * handled separately.
   */
  private RepositoryStatusCheckerThread repositoryStatusCheckerThread;

  /**
   * Remote storage context to store connection configs.
   */
  private RemoteStorageContext remoteStorageContext;

  // == set by configurators

  /**
   * The remote storage.
   */
  private RemoteRepositoryStorage remoteStorage;

  // == internals

  /**
   * Item content validators. Maintained by configurator, but map is final and created internally.
   */
  private final Map<String, ItemContentValidator> itemContentValidators = Maps.newHashMap();

  /**
   * The proxy remote status
   */
  private volatile RemoteStatus remoteStatus = RemoteStatus.UNKNOWN;

  /**
   * Last time remote status was updated
   */
  private volatile long remoteStatusUpdated = 0;

  /**
   * if remote url changed, need special handling after save
   */
  private volatile boolean remoteUrlChanged = false;

  /**
   * How much should be the last known remote status be retained.
   */
  private volatile NumberSequence remoteStatusRetainTimeSequence = new ConstantNumberSequence(
      REMOTE_STATUS_RETAIN_TIME);

  @Inject
  public void populateAbstractProxyRepository(ThreadPoolManager poolManager) {
    this.poolManager = checkNotNull(poolManager);

    // we have been not configured yet! So, we have no ID and stuff coming from config!
    // set here
    remoteStorageContext =
        new DefaultRemoteStorageContext(getApplicationConfiguration().getGlobalRemoteStorageContext());
  }

  @Override
  protected AbstractProxyRepositoryConfiguration getExternalConfiguration(boolean forModification) {
    return (AbstractProxyRepositoryConfiguration) getCurrentCoreConfiguration().getExternalConfiguration()
        .getConfiguration(
            forModification);
  }

  @Subscribe
  public void on(final NexusStoppedEvent e) {
    disposeRepositoryStatusCheckerThread();
  }

  private void createRepositoryStatusCheckerThread() {
    // only for proxy kind
    if (getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
      if (repositoryStatusCheckerThread == null) {
        repositoryStatusCheckerThread =
            new RepositoryStatusCheckerThread(LoggerFactory.getLogger(getClass().getName() + "-"
                + getId()), this);
        repositoryStatusCheckerThread.setRunning(true);
        repositoryStatusCheckerThread.setDaemon(true);
        repositoryStatusCheckerThread.start();
      }
    }
  }

  private void disposeRepositoryStatusCheckerThread() {
    // not depend on kind, as it might be "transformed" from proxy to hosted
    if (repositoryStatusCheckerThread != null) {
      repositoryStatusCheckerThread.setRunning(false);
      repositoryStatusCheckerThread.interrupt();
    }
  }

  @Override
  public void dispose() {
    super.dispose();
    // kill our daemon thread too, if needed
    disposeRepositoryStatusCheckerThread();
  }

  @Override
  protected void doConfigure()
      throws ConfigurationException
  {
    super.doConfigure();
    createRepositoryStatusCheckerThread();
  }

  @Override
  public boolean commitChanges()
      throws ConfigurationException
  {
    boolean result = super.commitChanges();

    if (result) {
      this.remoteUrlChanged = false;
    }

    return result;
  }

  @Override
  public boolean rollbackChanges() {
    this.remoteUrlChanged = false;

    return super.rollbackChanges();
  }

  @Override
  protected RepositoryConfigurationUpdatedEvent getRepositoryConfigurationUpdatedEvent() {
    RepositoryConfigurationUpdatedEvent event = super.getRepositoryConfigurationUpdatedEvent();

    event.setRemoteUrlChanged(this.remoteUrlChanged);

    return event;
  }

  @Override
  public final void expireProxyCaches(final ResourceStoreRequest request) {
    expireProxyCaches(request, null);
  }

  @Override
  public final boolean expireProxyCaches(final ResourceStoreRequest request, final WalkerFilter filter) {
    if (!shouldServiceOperation(request, "expireProxyCaches")) {
      return false;
    }
    log.debug("Expiring proxy caches in repository {} from path='{}'", this, request.getRequestPath());
    return doExpireProxyCaches(request, filter);
  }

  /**
   * Allows proxy cache invalidation to be disabled by system property.
   *
   * @since 2.9
   */
  private static final boolean PROXY_CACHE_INVALIDATION_TOKEN_DISABLED = SystemPropertiesHelper.getBoolean(
      AbstractProxyRepository.class.getName() + ".proxyCacheInvalidationToken.disabled", false);

  static {
    if (PROXY_CACHE_INVALIDATION_TOKEN_DISABLED) {
      LoggerFactory.getLogger(AbstractProxyRepository.class).info("Proxy-cache invalidation-token support disabled");
    }
  }

  /**
   * Token set when expire proxy caches is done for entire collection.
   *
   * @see #doExpireProxyCaches(ResourceStoreRequest, WalkerFilter)
   * @since 2.9
   */
  protected volatile String proxyCacheInvalidationToken;

  /**
   * Key for storage-item invalidation token attribute.
   *
   * @see #isOld(int, StorageItem, boolean)
   * @since 2.9
   */
  public static final String PROXY_CACHE_INVALIDATION_TOKEN_KEY = "proxyRepository-invalidationToken";

  protected boolean doExpireProxyCaches(final ResourceStoreRequest request, final WalkerFilter filter) {
    // skip unless this is a proxy repository
    if (!getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
      return false;
    }

    // normalize request path
    if (StringUtils.isEmpty(request.getRequestPath())) {
      request.setRequestPath(RepositoryItemUid.PATH_ROOT);
    }

    // flag to indicate if cache was altered or not
    boolean cacheAltered;

    // special handling for expiration for entire collection, unless disabled by system property
    if (!PROXY_CACHE_INVALIDATION_TOKEN_DISABLED && RepositoryItemUid.PATH_ROOT.equals(request.getRequestPath())) {
      // generate a unique token for this invalidation request
      proxyCacheInvalidationToken = String.valueOf(System.nanoTime());
      log.debug("Proxy cache marked as invalid for repository {}, token: {}", this, proxyCacheInvalidationToken);

      // assume the cache will alter, we can not know for sure w/o expensive walking operation
      cacheAltered = true;
    }
    else {
      // crawl the local storage (which is in this case proxy cache)
      // and flip the isExpired attribute bits to true
      request.setRequestLocalOnly(true);
      // 1st, expire all the files below path
      final DefaultWalkerContext ctx = new DefaultWalkerContext(this, request, filter);
      final ExpireCacheWalker expireCacheWalkerProcessor = new ExpireCacheWalker(this);
      ctx.getProcessors().add(expireCacheWalkerProcessor);
      try {
        getWalker().walk(ctx);
      }
      catch (WalkerException e) {
        if (!(e.getWalkerContext().getStopCause() instanceof ItemNotFoundException)) {
          // everything that is not ItemNotFound should be reported,
          // otherwise just neglect it
          throw e;
        }
      }
      cacheAltered = expireCacheWalkerProcessor.isCacheAltered();
    }

    if (log.isDebugEnabled()) {
      if (cacheAltered) {
        log.info("Proxy cache was expired for repository {} from path='{}'", this, request.getRequestPath());
      }
      else {
        log.debug("Proxy cache not altered for repository {} from path='{}'", this, request.getRequestPath());
      }
    }

    eventBus().post(new RepositoryEventExpireProxyCaches(
        this, request.getRequestPath(), request.getRequestContext().flatten(), cacheAltered));

    return cacheAltered;
  }

  protected boolean doExpireCaches(final ResourceStoreRequest request, final WalkerFilter filter) {
    // expire proxy cache
    boolean v1 = doExpireProxyCaches(request, filter);
    // do the stuff we inherited
    boolean v2 = super.doExpireCaches(request, filter);
    // return v1 OR v2
    return v1 || v2;
  }

  @Override
  protected Collection<String> doEvictUnusedItems(final ResourceStoreRequest request, final long timestamp) {
    if (getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
      Collection<String> result =
          doEvictUnusedItems(request, timestamp, new EvictUnusedItemsWalkerProcessor(timestamp),
              new EvictUnusedItemsWalkerFilter());
      eventBus().post(new RepositoryEventEvictUnusedItems(this));
      return result;
    }
    else {
      return super.doEvictUnusedItems(request, timestamp);
    }
  }

  protected Collection<String> doEvictUnusedItems(ResourceStoreRequest request, final long timestamp,
                                                  EvictUnusedItemsWalkerProcessor processor, WalkerFilter filter)
  {
    request.setRequestLocalOnly(true);
    DefaultWalkerContext ctx = new DefaultWalkerContext(this, request, filter);
    ctx.getProcessors().add(processor);
    // and let it loose
    try {
      getWalker().walk(ctx);
    }
    catch (WalkerException e) {
      if (!(e.getWalkerContext().getStopCause() instanceof ItemNotFoundException)) {
        // everything that is not ItemNotFound should be reported,
        // otherwise just neglect it
        throw e;
      }
    }

    return processor.getFiles();
  }

  @Override
  public Map<String, ItemContentValidator> getItemContentValidators() {
    return itemContentValidators;
  }

  @Override
  public boolean isFileTypeValidation() {
    return getExternalConfiguration(false).isFileTypeValidation();
  }

  @Override
  public void setFileTypeValidation(boolean doValidate) {
    getExternalConfiguration(true).setFileTypeValidation(doValidate);
  }

  @Override
  public boolean isItemAgingActive() {
    return getExternalConfiguration(false).isItemAgingActive();
  }

  @Override
  public void setItemAgingActive(boolean value) {
    getExternalConfiguration(true).setItemAgingActive(value);
  }

  @Override
  public boolean isAutoBlockActive() {
    return getExternalConfiguration(false).isAutoBlockActive();
  }

  @Override
  public void setAutoBlockActive(boolean val) {
    // NEXUS-3516: if user disables autoblock, and repo is auto-blocked, unblock it
    if (!val && ProxyMode.BLOCKED_AUTO.equals(getProxyMode())) {
      log.warn(
          String.format(
              "Proxy Repository %s was auto-blocked, but user disabled this feature. Unblocking repository, but this MAY cause Nexus to leak connections (if remote repository is still down)!",
              RepositoryStringUtils.getHumanizedNameString(this)));

      setProxyMode(ProxyMode.ALLOW);
    }

    getExternalConfiguration(true).setAutoBlockActive(val);
  }

  @Override
  public long getCurrentRemoteStatusRetainTime() {
    return this.remoteStatusRetainTimeSequence.peek();
  }

  @Override
  public long getNextRemoteStatusRetainTime() {
    // step it up, but topped
    if (this.remoteStatusRetainTimeSequence.peek() <= AUTO_BLOCK_STATUS_MAX_RETAIN_TIME) {
      // step it up
      return this.remoteStatusRetainTimeSequence.next();
    }
    else {
      // it is topped, so just return current
      return getCurrentRemoteStatusRetainTime();
    }
  }

  @Override
  public ProxyMode getProxyMode() {
    if (getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
      return getExternalConfiguration(false).getProxyMode();
    }
    else {
      return null;
    }
  }

  /**
   * ProxyMode is a persisted configuration property, hence it modifies configuration! It is the caller
   * responsibility
   * to save configuration.
   */
  protected void setProxyMode(ProxyMode proxyMode, boolean sendNotification, Throwable cause) {
    if (getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
      ProxyMode oldProxyMode = getProxyMode();

      // NEXUS-4537: apply transition constraints: BLOCKED_MANUALLY cannot be transitioned into BLOCKED_AUTO
      if (!(ProxyMode.BLOCKED_AUTO.equals(proxyMode) && ProxyMode.BLOCKED_MANUAL.equals(oldProxyMode))) {
        // change configuration only if we have a transition
        if (!oldProxyMode.equals(proxyMode)) {
          // NEXUS-3552: Tricking the config framework, we are making this applied _without_ making
          // configuration
          // dirty
          if (ProxyMode.BLOCKED_AUTO.equals(proxyMode) || ProxyMode.BLOCKED_AUTO.equals(oldProxyMode)) {
            getExternalConfiguration(false).setProxyMode(proxyMode);

            if (isDirty()) {
              // we are dirty, then just set same value in the "changed" one too
              getExternalConfiguration(true).setProxyMode(proxyMode);
            }
          }
          else {
            // this makes it dirty if it was not dirty yet, but this is the intention too
            getExternalConfiguration(true).setProxyMode(proxyMode);
          }
        }

        // setting the time to retain remote status, depending on proxy mode
        // if not blocked_auto, just use default as it was the case before AutoBlock
        if (ProxyMode.BLOCKED_AUTO.equals(proxyMode)) {
          if (!(this.remoteStatusRetainTimeSequence instanceof FibonacciNumberSequence)) {
            // take the timeout * 2 as initial step
            long initialStep = getRemoteConnectionSettings().getConnectionTimeout() * 2L;

            // make it a fibonacci one
            this.remoteStatusRetainTimeSequence = new FibonacciNumberSequence(initialStep);

            // make it step one
            this.remoteStatusRetainTimeSequence.next();

            // ping the monitor thread
            if (this.repositoryStatusCheckerThread != null) {
              this.repositoryStatusCheckerThread.interrupt();
            }
          }
        }
        else {
          this.remoteStatusRetainTimeSequence = new ConstantNumberSequence(REMOTE_STATUS_RETAIN_TIME);
        }

        // if this is proxy
        // and was !shouldProxy() and the new is shouldProxy()
        if (proxyMode != null && proxyMode.shouldProxy() && !oldProxyMode.shouldProxy()) {
          // NEXUS-4410: do this only when we are going BLOCKED_MANUAL -> ALLOW transition
          // In case of Auto unblocking, do not perform purge!
          if (!oldProxyMode.shouldAutoUnblock()) {
            if (log.isDebugEnabled()) {
              log.debug("We have a BLOCKED_MANUAL -> ALLOW transition, purging NFC");
            }

            getNotFoundCache().purge();
          }

          resetRemoteStatus();
        }

        if (sendNotification) {
          // this one should be fired _always_
          eventBus().post(new RepositoryEventProxyModeSet(this, oldProxyMode, proxyMode, cause));

          if (proxyMode != null && !proxyMode.equals(oldProxyMode)) {
            // this one should be fired on _transition_ only
            eventBus().post(new RepositoryEventProxyModeChanged(this, oldProxyMode, proxyMode, cause));
          }
        }
      }
    }
  }

  @Override
  public void setProxyMode(ProxyMode proxyMode) {
    setProxyMode(proxyMode, true, null);
  }

  /**
   * This method should be called by AbstractProxyRepository and it's descendants only. Since this method modifies
   * the
   * ProxyMode property of this repository, and this property is part of configuration, this call will result in
   * configuration flush too (potentially saving any other unsaved changes)!
   */
  protected void autoBlockProxying(Throwable cause) {
    // depend of proxy mode
    ProxyMode oldProxyMode = getProxyMode();

    // Detect do we deal with S3 remote peer, those are not managed/autoblocked, since we have no
    // proper means using HTTP only to detect the issue.
    {
      RemoteRepositoryStorage remoteStorage = getRemoteStorage();

      /**
       * Special case here to handle Amazon S3 storage. Problem is that if we do a request against a folder, a 403
       * will always be returned, as S3 doesn't support that. So we simple check if its s3 and if so, we ignore
       * the fact that 403 was returned (only in regards to auto-blocking, rest of system will still handle 403
       * response as expected)
       */
      try {
        if (remoteStorage instanceof AbstractHTTPRemoteRepositoryStorage
            && ((AbstractHTTPRemoteRepositoryStorage) remoteStorage).isRemotePeerAmazonS3Storage(this)
            && cause instanceof RemoteAccessDeniedException) {
          log.debug(
              "Not autoblocking repository id " + getId() + "since this is Amazon S3 proxy repo");
          return;
        }
      }
      catch (StorageException e) {
        // This shouldn't occur, since we are just checking the context
        log.debug("Unable to validate if proxy repository id " + getId() + "is Amazon S3", e);
      }
    }

    // invalidate remote status
    final String unavailableReason = parseRemoteUnavailableReason(cause);
    if (unavailableReason == null) {
      setRemoteStatus(RemoteStatus.UNAVAILABLE, cause);
    }
    else {
      setRemoteStatus(new RemoteStatus(RemoteStatus.Type.UNAVAILABLE, unavailableReason), cause);
    }

    // do we need to do anything at all?
    boolean autoBlockActive = isAutoBlockActive();

    // nag only here
    if (!ProxyMode.BLOCKED_AUTO.equals(oldProxyMode)) {
      StringBuilder sb = new StringBuilder();

      sb.append("Remote peer of proxy repository " + RepositoryStringUtils.getHumanizedNameString(this)
          + " threw a " + cause.getClass().getName() + " exception.");

      if (cause instanceof RemoteAccessException) {
        sb.append(" Please set up authorization information for this repository.");
      }
      else if (cause instanceof StorageException) {
        sb.append(" Connection/transport problems occured while connecting to remote peer of the repository.");
      }

      // nag about autoblock if needed
      if (autoBlockActive) {
        sb.append(" Auto-blocking this repository to prevent further connection-leaks and known-to-fail outbound"
            + " connections until administrator fixes the problems, or Nexus detects remote repository as healthy.");
      }

      // log the event
      if (log.isDebugEnabled()) {
        log.warn(sb.toString(), cause);
      }
      else {
        sb.append(" - Cause(s): ").append(cause.getMessage());

        Throwable c = cause.getCause();

        while (c != null) {
          sb.append(" > ").append(c.getMessage());

          c = c.getCause();
        }

        log.warn(sb.toString());
      }
    }

    // autoblock if needed (above is all about nagging)
    if (autoBlockActive) {
      if (oldProxyMode != null) {
        setProxyMode(ProxyMode.BLOCKED_AUTO, true, cause);
      }

      // NEXUS-3552: Do NOT save configuration, just make it applied (see setProxyMode() how it is done)
      // save configuration only if we made a transition, otherwise no save is needed
      // if ( oldProxyMode != null && !oldProxyMode.equals( ProxyMode.BLOCKED_AUTO ) )
      // {
      // try
      // {
      // // NEXUS-3552: Do NOT save configuration, just make it applied
      // getApplicationConfiguration().saveConfiguration();
      // }
      // catch ( IOException e )
      // {
      // log.warn(
      // "Cannot save configuration after AutoBlocking repository \"" + getName() + "\" (id=" + getId()
      // + ")", e );
      // }
      // }
    }
  }

  /**
   * This method should be called by AbstractProxyRepository and it's descendants only. Since this method modifies
   * the
   * ProxyMode property of this repository, and this property is part of configuration, this call will result in
   * configuration flush too (potentially saving any other unsaved changes)!
   */
  protected void autoUnBlockProxying() {
    setRemoteStatus(RemoteStatus.AVAILABLE, null);

    ProxyMode oldProxyMode = getProxyMode();

    if (oldProxyMode.shouldAutoUnblock()) {
      // log the event
      log.warn(
          String.format(
              "Remote peer of proxy repository %s detected as healthy, un-blocking the proxy repository (it was AutoBlocked by Nexus).",
              RepositoryStringUtils.getHumanizedNameString(this)));

      setProxyMode(ProxyMode.ALLOW, true, null);
    }

    // NEXUS-3552: Do NOT save configuration, just make it applied (see setProxyMode() how it is done)
    // try
    // {
    // getApplicationConfiguration().saveConfiguration();
    // }
    // catch ( IOException e )
    // {
    // log.warn(
    // "Cannot save configuration after AutoBlocking repository \"" + getName() + "\" (id=" + getId() + ")", e );
    // }
  }

  /**
   * Best effort to extract reason why remote is not available.
   *
   * @param cause cause why the remote is not available (can be null)
   * @return parsed reason or null if reason could not be parsed
   */
  protected String parseRemoteUnavailableReason(final Throwable cause) {
    if (cause == null) {
      return null;
    }
    if (cause.getCause() != null) {
      if (cause.getCause() instanceof SSLPeerUnverifiedException) {
        return "Untrusted Remote";
      }
      if (cause.getCause() instanceof SSLException) {
        return cause.getCause().getMessage();
      }
    }

    return null;
  }

  @Override
  public RepositoryStatusCheckMode getRepositoryStatusCheckMode() {
    return getExternalConfiguration(false).getRepositoryStatusCheckMode();
  }

  @Override
  public void setRepositoryStatusCheckMode(RepositoryStatusCheckMode mode) {
    getExternalConfiguration(true).setRepositoryStatusCheckMode(mode);
  }

  @Override
  public String getRemoteUrl() {
    if (getCurrentConfiguration(false).getRemoteStorage() != null) {
      return getCurrentConfiguration(false).getRemoteStorage().getUrl();
    }
    else {
      return null;
    }
  }

  @Override
  public void setRemoteUrl(String remoteUrl)
      throws RemoteStorageException
  {
    if (getRemoteStorage() != null) {
      String newRemoteUrl = remoteUrl.trim();

      String oldRemoteUrl = getRemoteUrl();

      if (!newRemoteUrl.endsWith(RepositoryItemUid.PATH_SEPARATOR)) {
        newRemoteUrl = newRemoteUrl + RepositoryItemUid.PATH_SEPARATOR;
      }

      getRemoteStorage().validateStorageUrl(newRemoteUrl);

      getCurrentConfiguration(true).getRemoteStorage().setUrl(newRemoteUrl);

      if ((StringUtils.isEmpty(oldRemoteUrl) && StringUtils.isNotEmpty(newRemoteUrl))
          || (StringUtils.isNotEmpty(oldRemoteUrl) && !oldRemoteUrl.equals(newRemoteUrl))) {
        this.remoteUrlChanged = true;
      }
    }
    else {
      throw new RemoteStorageException("No remote storage set on repository \"" + getName() + "\" (ID=\""
          + getId() + "\"), cannot set remoteUrl!");
    }
  }

  /**
   * Gets the item max age in (in minutes).
   *
   * @return the item max age in (in minutes)
   */
  @Override
  public int getItemMaxAge() {
    return getExternalConfiguration(false).getItemMaxAge();
  }

  /**
   * Sets the item max age in (in minutes).
   *
   * @param itemMaxAge the new item max age in (in minutes).
   */
  @Override
  public void setItemMaxAge(int itemMaxAge) {
    getExternalConfiguration(true).setItemMaxAge(itemMaxAge);
  }

  protected void resetRemoteStatus() {
    remoteStatusUpdated = 0;
  }

  /**
   * Is checking in progress?
   */
  private volatile boolean _remoteStatusChecking = false;

  @Override
  public RemoteStatus getRemoteStatus(ResourceStoreRequest request, boolean forceCheck) {
    // if the last known status is old, simply reset it
    if (forceCheck || System.currentTimeMillis() - remoteStatusUpdated > REMOTE_STATUS_RETAIN_TIME) {
      remoteStatus = RemoteStatus.UNKNOWN;
    }

    if (getProxyMode() != null && RemoteStatus.UNKNOWN.equals(remoteStatus) && !_remoteStatusChecking) {
      // check for thread and go check it
      _remoteStatusChecking = true;

      poolManager.getRepositoryThreadPool(this).submit(new RemoteStatusUpdateCallable(request));
    }

    return remoteStatus;
  }

  private void setRemoteStatus(RemoteStatus remoteStatus, Throwable cause) {
    this.remoteStatus = remoteStatus;

    // UNKNOWN does not count
    if (RemoteStatus.AVAILABLE.equals(remoteStatus) || RemoteStatus.UNAVAILABLE.equals(remoteStatus)) {
      this.remoteStatusUpdated = System.currentTimeMillis();
    }
  }

  @Override
  public RemoteStorageContext getRemoteStorageContext() {
    return remoteStorageContext;
  }

  @Override
  public RemoteConnectionSettings getRemoteConnectionSettings() {
    return getRemoteStorageContext().getRemoteConnectionSettings();
  }

  @Override
  public void setRemoteConnectionSettings(RemoteConnectionSettings settings) {
    getRemoteStorageContext().setRemoteConnectionSettings(settings);
  }

  @Override
  public RemoteAuthenticationSettings getRemoteAuthenticationSettings() {
    return getRemoteStorageContext().getRemoteAuthenticationSettings();
  }

  @Override
  public void setRemoteAuthenticationSettings(RemoteAuthenticationSettings settings) {
    getRemoteStorageContext().setRemoteAuthenticationSettings(settings);

    if (getProxyMode() != null && getProxyMode().shouldAutoUnblock()) {
      // perm changes? retry if autoBlocked
      setProxyMode(ProxyMode.ALLOW);
    }
  }

  @Override
  public RemoteRepositoryStorage getRemoteStorage() {
    return remoteStorage;
  }

  @Override
  public void setRemoteStorage(RemoteRepositoryStorage remoteStorage) {
    this.remoteStorage = remoteStorage;

    if (remoteStorage == null) {
      getCurrentConfiguration(true).setRemoteStorage(null);
    }
    else {
      if (getCurrentConfiguration(true).getRemoteStorage() == null) {
        getCurrentConfiguration(true).setRemoteStorage(new CRemoteStorage());
      }

      getCurrentConfiguration(true).getRemoteStorage().setProvider(remoteStorage.getProviderId());

      setWritePolicy(RepositoryWritePolicy.READ_ONLY);
    }
  }

  @Override
  public AbstractStorageItem doCacheItem(AbstractStorageItem item)
      throws LocalStorageException
  {
    AbstractStorageItem result = null;

    try {
      if (log.isDebugEnabled()) {
        log.debug(
            "Caching item " + item.getRepositoryItemUid().toString() + " in local storage of repository.");
      }

      final RepositoryItemUidLock itemLock = item.getRepositoryItemUid().getLock();

      itemLock.lock(Action.create);

      final Action action;

      try {
        action = getResultingActionOnWrite(item.getResourceStoreRequest());

        getLocalStorage().storeItem(this, item);

        removeFromNotFoundCache(item.getResourceStoreRequest());

        // we swapped the remote item with the one from local storage
        // using this method below, we ensure that we get a "wrapped"
        // content locator that will keel shared-lock on the content
        // until being fully read
        result = doRetrieveLocalItem(item.getResourceStoreRequest());

      }
      finally {
        itemLock.unlock();
      }

      result.getItemContext().setParentContext(item.getItemContext());

      if (Action.create.equals(action)) {
        eventBus().post(new RepositoryItemEventCacheCreate(this, result));
      }
      else {
        eventBus().post(new RepositoryItemEventCacheUpdate(this, result));
      }
    }
    catch (ItemNotFoundException ex) {
      log.warn(
          "Nexus BUG in "
              + RepositoryStringUtils.getHumanizedNameString(this)
              + ", ItemNotFoundException during cache! Please report this issue along with the stack trace below!",
          ex);

      // this is a nonsense, we just stored it!
      result = item;
    }
    catch (UnsupportedStorageOperationException ex) {
      log.warn(
          "LocalStorage or repository " + RepositoryStringUtils.getHumanizedNameString(this)
              + " does not handle STORE operation, not caching remote fetched item.", ex);

      result = item;
    }

    return result;
  }

  @Override
  protected StorageItem doRetrieveItem(ResourceStoreRequest request)
      throws IllegalOperationException, ItemNotFoundException, StorageException
  {
    if (log.isDebugEnabled()) {
      StringBuilder db = new StringBuilder(request.toString());

      db.append(" :: localOnly=").append(request.isRequestLocalOnly());
      db.append(", remoteOnly=").append(request.isRequestRemoteOnly());
      db.append(", asExpired=").append(request.isRequestAsExpired());

      if (getProxyMode() != null) {
        db.append(", ProxyMode=" + getProxyMode().toString());
      }

      log.debug(db.toString());
    }

    // we have to re-set locking here explicitly, since we are going to
    // make a "salto-mortale" here, see below
    // we start with "usual" read lock, we still don't know is this hosted or proxy repo
    // if proxy, we still don't know do we have to go remote (local copy is old) or not
    // if proxy and need to go remote, we want to _protect_ ourselves from
    // serving up partial downloads...

    final RepositoryItemUid itemUid = createUid(request.getRequestPath());

    final RepositoryItemUidLock itemUidLock = itemUid.getLock();

    itemUidLock.lock(Action.read);

    try {
      if (!getRepositoryKind().isFacetAvailable(ProxyRepository.class)) {
        // we have no proxy facet, just get 'em!
        return super.doRetrieveItem(request);
      }
      else {
        // we have Proxy facet, so we want to check carefully local storage
        // Reason: a previous thread may still _downloading_ the stuff we want to
        // serve to another client, so we have to _wait_ for download, but for download
        // only.
        AbstractStorageItem localItem = null;

        if (!request.isRequestRemoteOnly()) {
          try {
            localItem = (AbstractStorageItem) super.doRetrieveItem(request);

            if (localItem != null && !request.isRequestAsExpired() && !isOld(localItem)) {
              // local copy is just fine, so, we are proxy but we have valid local copy in cache
              return localItem;
            }
          }
          catch (ItemNotFoundException e) {
            localItem = null;
          }
        }

        // we are a proxy, and we either don't have local copy or is stale, we need to
        // go remote and potentially check for new version of file, but we still don't know
        // will we actually fetch it (since aging != remote file changed!)
        // BUT, from this point on, we want to _serialize_ access, so upgrade to CREATE lock

        itemUidLock.lock(Action.create);

        try {
          // check local copy again, we were maybe blocked for a download, and we need to
          // recheck local copy after we acquired exclusive lock
          if (!request.isRequestRemoteOnly()) {
            try {
              localItem = (AbstractStorageItem) super.doRetrieveItem(request);

              if (localItem != null && !request.isRequestAsExpired() && !isOld(localItem)) {
                // local copy is just fine (downloaded by a thread holding us blocked on acquiring
                // exclusive lock)
                return localItem;
              }
            }
            catch (ItemNotFoundException e) {
              localItem = null;
            }
          }

          // this whole method happens with exclusive lock on UID
          return doRetrieveItem0(request, localItem);
        }
        finally {
          itemUidLock.unlock();
        }
      }
    }
    finally {
      itemUidLock.unlock();
    }
  }

  protected void shouldTryRemote(final ResourceStoreRequest request)
      throws IllegalOperationException, ItemNotFoundException
  {
    if (request.isRequestLocalOnly()) {
      throw new ItemNotFoundException(ItemNotFoundException.reasonFor(request, this,
          "Request is marked as local-only, remote access not allowed from %s", this));
    }
    if (getProxyMode() != null && !getProxyMode().shouldProxy()) {
      throw new ItemNotFoundException(ItemNotFoundException.reasonFor(request, this,
          "Repository proxy-mode is %s, remote access not allowed from %s", getProxyMode(), this));
    }
  }

  protected StorageItem doRetrieveItem0(ResourceStoreRequest request, AbstractStorageItem localItem)
      throws IllegalOperationException, ItemNotFoundException, StorageException
  {
    AbstractStorageItem item = null;
    AbstractStorageItem remoteItem = null;

    // proxyMode and request.localOnly decides 1st
    ItemNotFoundException noRemoteAccessReason = null;
    try {
      shouldTryRemote(request);
    }
    catch (ItemNotFoundException e) {
      noRemoteAccessReason = e;
    }

    if (noRemoteAccessReason == null) {
      for (RequestStrategy strategy : getRegisteredStrategies().values()) {
        try {
          strategy.onRemoteAccess(this, request, localItem);
        }
        catch (ItemNotFoundException e) {
          noRemoteAccessReason = e;
          // escape
          break;
        }
      }
    }

    if (noRemoteAccessReason == null) {
      // we are able to go remote
      if (localItem == null || request.isRequestAsExpired() || isOld(localItem)) {
        // we should go remote coz we have no local copy or it is old
        try {
          boolean shouldGetRemote = false;

          if (localItem != null) {
            if (log.isDebugEnabled()) {
              log.debug(
                  "Item " + request.toString()
                      + " is old, checking for newer file on remote then local: "
                      + new Date(localItem.getModified()));
            }

            // check is the remote newer than the local one
            try {
              shouldGetRemote = doCheckRemoteItemExistence(localItem, request);

              if (!shouldGetRemote) {
                markItemRemotelyChecked(localItem);

                if (log.isDebugEnabled()) {
                  log.debug(
                      "No newer version of item " + request.toString() + " found on remote storage.");
                }
              }
              else {
                if (log.isDebugEnabled()) {
                  log.debug(
                      "Newer version of item " + request.toString() + " is found on remote storage.");
                }
              }

            }
            catch (RemoteStorageException ex) {
              // NEXUS-4593 HTTP status 403 should not lead to autoblock
              if (!(ex instanceof RemoteAccessDeniedException)
                  && !(ex instanceof RemoteStorageTransportException)) {
                autoBlockProxying(ex);
              }

              if (ex instanceof RemoteStorageTransportException) {
                throw ex;
              }

              // do not go remote, but we did not mark it as "remote checked" also.
              // let the user do proper setup and probably it will try again
              shouldGetRemote = false;
            }
            catch (IOException ex) {
              // do not go remote, but we did not mark it as "remote checked" also.
              // let the user do proper setup and probably it will try again
              shouldGetRemote = false;
            }
          }
          else {
            // we have no local copy of it, try to get it unconditionally
            shouldGetRemote = true;
          }

          if (shouldGetRemote) {
            // this will GET it unconditionally
            try {
              remoteItem = doRetrieveRemoteItem(request);

              if (log.isDebugEnabled()) {
                log.debug("Item " + request.toString() + " found in remote storage.");
              }
            }
            catch (StorageException ex) {
              if (ex instanceof RemoteStorageException
                  // NEXUS-4593 HTTP status 403 should not lead to autoblock
                  && !(ex instanceof RemoteAccessDeniedException)
                  && !(ex instanceof RemoteStorageTransportException)) {
                autoBlockProxying(ex);
              }

              if (ex instanceof RemoteStorageTransportException
                  || ex instanceof LocalStorageEOFException) {
                throw ex;
              }

              if (ex instanceof RemoteAccessDeniedException) {
                log.debug("Error code 403 {} obtaining {} from remote storage.", ex.getMessage(), request);
                request.getRequestContext().put("remote.accessDeniedException", ex);
              }

              remoteItem = null;

              // cleanup if any remnant is here
              try {
                if (localItem == null) {
                  deleteItem(false, request);
                }
              }
              catch (ItemNotFoundException ex1) {
                // ignore
              }
              catch (UnsupportedStorageOperationException ex2) {
                // will not happen
              }
            }
          }
          else {
            remoteItem = null;
          }
        }
        catch (ItemNotFoundException ex) {
          if (log.isDebugEnabled()) {
            log.debug("Item " + request.toString() + " not found in remote storage.");
          }

          remoteItem = null;
        }
      }

      if (localItem == null && remoteItem == null) {
        // we dont have neither one, NotFoundException
        if (log.isDebugEnabled()) {
          log.debug(
              "Item " + request.toString()
                  + " does not exist in local or remote storage, throwing ItemNotFoundException.");
        }

        throw new ItemNotFoundException(reasonFor(request, this,
            "Path %s not found in local nor in remote storage of %s", request.getRequestPath(),
            this));
      }
      else if (localItem != null && remoteItem == null) {
        // simple: we have local but not remote (coz we are offline or coz it is not newer)
        if (log.isDebugEnabled()) {
          log.debug(
              "Item " + request.toString()
                  + " does exist in local storage and is fresh, returning local one.");
        }

        item = localItem;
      }
      else {
        // the fact that remoteItem != null means we _have_ to return that one
        // OR: we had no local copy
        // OR: remoteItem is for sure newer (look above)
        item = remoteItem;
      }

    }
    else {
      // we cannot go remote
      if (localItem != null) {
        if (log.isDebugEnabled()) {
          log.debug(
              "Item " + request.toString() + " does exist locally and cannot go remote, returning local one.");
        }

        item = localItem;
      }
      else {
        if (log.isDebugEnabled()) {
          log.debug(
              "Item " + request.toString()
                  + " does not exist locally and cannot go remote, throwing ItemNotFoundException.");
        }

        throw new ItemNotFoundException(ItemNotFoundException.reasonFor(request, this,
            noRemoteAccessReason.getMessage()), noRemoteAccessReason);
      }
    }

    return item;
  }

  private void sendContentValidationEvents(ResourceStoreRequest request, List<RepositoryItemValidationEvent> events,
                                           boolean isContentValid)
  {
    if (log.isDebugEnabled() && !isContentValid) {
      log.debug("Item " + request.toString() + " failed content integrity validation.");
    }

    for (RepositoryItemValidationEvent event : events) {
      eventBus().post(event);
    }
  }

  protected void markItemRemotelyChecked(final StorageItem item)
      throws IOException, ItemNotFoundException
  {
    // remote file unchanged, touch the local one to renew it's Age
    getAttributesHandler().touchItemCheckedRemotely(System.currentTimeMillis(), item);
  }

  /**
   * Validates integrity of content of <code>item</code>. Retruns <code>true</code> if item content is valid and
   * <code>false</code> if item content is corrupted. Note that this method is called doRetrieveRemoteItem, so
   * implementation must retrieve checksum files directly from remote storage <code>
   * getRemoteStorage().retrieveItem( this, context, getRemoteUrl(), checksumUid.getPath() );
   * </code>
   */
  protected boolean doValidateRemoteItemContent(ResourceStoreRequest req, String baseUrl, AbstractStorageItem item,
                                                List<RepositoryItemValidationEvent> events)
  {
    boolean isValid = true;

    for (Map.Entry<String, ItemContentValidator> icventry : getItemContentValidators().entrySet()) {
      try {
        boolean isValidByCurrentItemContentValidator =
            icventry.getValue().isRemoteItemContentValid(this, req, baseUrl, item, events);

        if (!isValidByCurrentItemContentValidator) {
          log.info(
              String.format(
                  "Proxied item %s evaluated as INVALID during content validation (validator=%s, sourceUrl=%s)",
                  item.getRepositoryItemUid().toString(), icventry.getKey(), item.getRemoteUrl()));
        }

        isValid = isValid && isValidByCurrentItemContentValidator;
      }
      catch (StorageException e) {
        log.info(
            String.format(
                "Proxied item %s evaluated as INVALID during content validation (validator=%s, sourceUrl=%s)",
                item.getRepositoryItemUid().toString(), icventry.getKey(), item.getRemoteUrl()), e);

        isValid = false;
      }
    }

    return isValid;
  }

  /**
   * Checks for remote existence of local item.
   */
  protected boolean doCheckRemoteItemExistence(StorageItem localItem, ResourceStoreRequest request)
      throws RemoteAccessException, RemoteStorageException
  {
    if (localItem != null) {
      return getRemoteStorage().containsItem(localItem.getModified(), this, request);
    }
    else {
      return getRemoteStorage().containsItem(this, request);
    }
  }

  /**
   * Retrieves item with specified uid from remote storage according to the following retry-fallback-blacklist rules.
   * <li>Only retrieve item operation will use mirrors, other operations, like check availability and retrieve
   * checksum file, will always use repository canonical url.</li> <li>Only one mirror url will be considered before
   * retrieve item operation falls back to repository canonical url.</li> <li>Repository canonical url will never be
   * put on the blacklist.</li> <li>If retrieve item operation fails with ItemNotFound or AccessDenied error, the
   * operation will be retried with another url or original error will be reported if there are no more urls.</li>
   * <li>
   * If retrieve item operation fails with generic StorageException or item content is corrupt, the operation will be
   * retried one more time from the same url. After that, the operation will be retried with another url or original
   * error will be returned if there are no more urls.</li> <li>Mirror url will be put on the blacklist if retrieve
   * item operation from the url failed with StorageException, AccessDenied or InvalidItemContent error but the item
   * was successfully retrieve from another url.</li> <li>Mirror url will be removed from blacklist after 30
   * minutes.</li>
   * The following matrix summarises retry/blacklist behaviour
   * <p/>
   * <p/>
   *
   * <pre>
   * Error condition      Retry?        Blacklist?
   *
   * InetNotFound         no            no
   * AccessDedied         no            yes
   * InvalidContent       no            no
   * Other                yes           yes
   * </pre>
   */
  protected AbstractStorageItem doRetrieveRemoteItem(ResourceStoreRequest request)
      throws ItemNotFoundException, RemoteAccessException, StorageException
  {
    final RepositoryItemUid itemUid = createUid(request.getRequestPath());

    final RepositoryItemUidLock itemUidLock = itemUid.getLock();

    // all this remote download happens in exclusive lock
    itemUidLock.lock(Action.create);

    try {
      List<String> remoteUrls = getRemoteUrls(request);

      List<RepositoryItemValidationEvent> events = new ArrayList<>();

      Exception lastException = null;

      all_urls:
      for (String remoteUrl : remoteUrls) {
        int retryCount = 1;

        if (getRemoteStorageContext() != null) {
          RemoteConnectionSettings settings = getRemoteStorageContext().getRemoteConnectionSettings();
          if (settings != null) {
            retryCount = settings.getRetrievalRetryCount();
          }
        }

        if (log.isDebugEnabled()) {
          log.debug("Using URL:" + remoteUrl + ", retryCount=" + retryCount);
        }

        // Validate the mirror URL
        try {
          getRemoteStorage().validateStorageUrl(remoteUrl);
        }
        catch (RemoteStorageException e) {
          lastException = e;

          logFailedUrl(remoteUrl, e);

          continue all_urls; // retry with next url
        }
        catch (Exception e) {
          lastException = e;

          // make it logged, this is RuntimeEx
          log.warn("Failed URL validation: {}", remoteUrl, e);

          continue all_urls; // retry with next url
        }

        for (int i = 0; i < retryCount; i++) {
          try {
            // events.clear();

            AbstractStorageItem remoteItem =
                getRemoteStorage().retrieveItem(this, request, remoteUrl);

            remoteItem = doCacheItem(remoteItem);

            if (doValidateRemoteItemContent(request, remoteUrl, remoteItem, events)) {
              sendContentValidationEvents(request, events, true);

              return remoteItem;
            }
            else {
              continue all_urls; // retry with next url
            }
          }
          catch (ItemNotFoundException e) {
            lastException = e;

            continue all_urls; // retry with next url
          }
          catch (RemoteAccessException e) {
            lastException = e;

            logFailedUrl(remoteUrl, e);

            continue all_urls; // retry with next url
          }
          catch (RemoteStorageException e) {
            // in case when we were unable to make outbound request
            // at all, do not retry
            if (e instanceof RemoteStorageTransportException) {
              throw e;
            }

            lastException = e;

            // debug, print all
            if (log.isDebugEnabled()) {
              logFailedUrl(remoteUrl, e);
            }
            // not debug, only print the message
            else {
              Throwable t = ExceptionUtils.getRootCause(e);

              if (t == null) {
                t = e;
              }

              log.error(
                  String.format(
                      "Got RemoteStorageException in proxy repository %s while retrieving remote artifact \"%s\" from URL %s, this is %s (re)try, cause: %s: %s",
                      RepositoryStringUtils.getHumanizedNameString(this), request.toString(),
                      remoteUrl, String.valueOf(i + 1), t.getClass().getName(),
                      t.getMessage()));
            }
            // do not switch url yet, obey the retries
          }
          catch (LocalStorageException e) {
            lastException = e;

            // debug, print all
            if (log.isDebugEnabled()) {
              logFailedUrl(remoteUrl, e);
            }
            // not debug, only print the message
            else {
              Throwable t = ExceptionUtils.getRootCause(e);

              if (t == null) {
                t = e;
              }

              log.error(
                  "Got LocalStorageException in proxy repository {} while caching retrieved artifact \"{}\" got from URL {}, will attempt next mirror",
                  RepositoryStringUtils.getHumanizedNameString(this),
                  request,
                  remoteUrl,
                  t
              );
            }
            // do not switch url yet, obey the retries
            // TODO: IOException _might_ be actually a fatal error (like Nx process have no perms to write to disk)
            // but also might come when caching, from inability to READ the HTTP response body (see NEXUS-5898)
            // Hence, we will retry here too, and in case of first type of IO problems no harm will be done
            // anyway, but will solve the second type of problems, where retry will be attempted
          }
          catch (RuntimeException e) {
            lastException = e;

            // make it logged, this is RuntimeEx
            log.warn("Failed URL retrieve/cache: {}", remoteUrl, e);

            continue all_urls; // retry with next url
          }

          // retry with same url
        }
      }

      // if we got here, requested item was not retrieved for some reason

      sendContentValidationEvents(request, events, false);

      try {
        getLocalStorage().deleteItem(this, request);
      }
      catch (ItemNotFoundException e) {
        // good, we want this item deleted
      }
      catch (UnsupportedStorageOperationException e) {
        log.warn("Unexpected Exception in " + RepositoryStringUtils.getHumanizedNameString(this), e);
      }

      if (lastException instanceof StorageException) {
        throw (StorageException) lastException;
      }
      else if (lastException instanceof ItemNotFoundException) {
        throw (ItemNotFoundException) lastException;
      }

      // validation failed, I guess.
      throw new ItemNotFoundException(reasonFor(request, this,
          "Path %s fetched from remote storage of %s but failed validation.", request.getRequestPath(),
          this));
    }
    finally {
      itemUidLock.unlock();
    }
  }

  protected List<String> getRemoteUrls(final ResourceStoreRequest request) {
    return Lists.newArrayList(getRemoteUrl());
  }

  private void logFailedUrl(String url, Exception e) {
    if (log.isDebugEnabled()) {
      log.debug("Failed URL: {}", url, e);
    }
  }

  /**
   * Checks if item is old with "default" maxAge.
   *
   * @param item the item
   * @return true, if it is old
   */
  protected boolean isOld(StorageItem item) {
    return isOld(getItemMaxAge(), item);
  }

  /**
   * Checks if item is old with given maxAge.
   */
  protected boolean isOld(int maxAge, StorageItem item) {
    return isOld(maxAge, item, isItemAgingActive());
  }

  protected boolean isOld(int maxAge, StorageItem item, boolean shouldCalculate) {
    if (!shouldCalculate) {
      // simply say "is old" always
      return true;
    }

    // If entire proxy cache has been invalidated, lazily expire item
    String itemInvalidationToken = item.getRepositoryItemAttributes().get(PROXY_CACHE_INVALIDATION_TOKEN_KEY);
    if (proxyCacheInvalidationToken != null && !proxyCacheInvalidationToken.equals(itemInvalidationToken)) {
      log.debug("Item treated as expired due to proxy-cache invalidation token mismatch: {} != {}; item: {}",
          proxyCacheInvalidationToken, itemInvalidationToken, item);
      // if item does not carry the current proxy cache invalidation token, then treat it as expired
      item.setExpired(true);
      item.getRepositoryItemAttributes().put(PROXY_CACHE_INVALIDATION_TOKEN_KEY, proxyCacheInvalidationToken);

      // save attributes, surrounding usage of item reloads attributes (over and over for some reason)
      try {
        getAttributesHandler().storeAttributes(item);
      }
      catch (IOException e) {
        throw Throwables.propagate(e);
      }
      return true;
    }

    // if item is manually expired, true
    if (item.isExpired()) {
      return true;
    }

    // a directory is not "aged"
    if (StorageCollectionItem.class.isAssignableFrom(item.getClass())) {
      return false;
    }

    // if repo is non-expirable, false
    if (maxAge < 0) {
      return false;
    }
    // else check age
    else {
      return ((System.currentTimeMillis() - item.getRemoteChecked()) > (maxAge * 60L * 1000L));
    }
  }

  private class RemoteStatusUpdateCallable
      implements Callable<Object>
  {

    private ResourceStoreRequest request;

    public RemoteStatusUpdateCallable(ResourceStoreRequest request) {
      this.request = request;
    }

    public Object call()
        throws Exception
    {
      try {
        try {
          if (!getProxyMode().shouldCheckRemoteStatus()) {
            setRemoteStatus(
                RemoteStatus.UNAVAILABLE,
                new ItemNotFoundException(reasonFor(request, AbstractProxyRepository.this,
                    "Proxy mode %s or repository %s forbids remote storage use.", getProxyMode(),
                    AbstractProxyRepository.this)));
          }
          else {
            if (isRemoteStorageReachable(request)) {
              autoUnBlockProxying();
            }
            else {
              autoBlockProxying(new ItemNotFoundException(reasonFor(request,
                  AbstractProxyRepository.this, "Remote peer of repository %s detected as unavailable.",
                  AbstractProxyRepository.this)));
            }
          }
        }
        catch (RemoteStorageException e) {
          // autoblock only when remote problems occur
          autoBlockProxying(e);
        }
      }
      finally {
        _remoteStatusChecking = false;
      }

      return null;
    }
  }

  protected boolean isRemoteStorageReachable(ResourceStoreRequest request)
      throws StorageException
  {
    return getRemoteStorage().isReachable(this, request);
  }

  // Need to allow delete for proxy repos
  @Override
  protected boolean isActionAllowedReadOnly(Action action) {
    return action.equals(Action.read) || action.equals(Action.delete);
  }

  /**
   * Beside original behavior, only add to NFC when we are not in BLOCKED mode.
   *
   * @since 2.0
   */
  @Override
  protected boolean shouldAddToNotFoundCache(final ResourceStoreRequest request) {
    boolean shouldAddToNFC = super.shouldAddToNotFoundCache(request);
    if (shouldAddToNFC) {
      shouldAddToNFC = getProxyMode() == null || getProxyMode().shouldProxy();
      if (!shouldAddToNFC && log.isDebugEnabled()) {
        log.debug(
            String.format(
                "Proxy repository '%s' is is not allowed to issue remote requests (%s), not adding path '%s' to NFC",
                getId(), getProxyMode(), request.getRequestPath()));
      }
    }
    return shouldAddToNFC;
  }

}
TOP

Related Classes of org.sonatype.nexus.proxy.repository.AbstractProxyRepository

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.