LifeCyclePolicyScanner.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.opencastproject.lifecyclemanagement.impl.scanner;

import static org.opencastproject.util.ReadinessIndicator.ARTIFACT;

import org.opencastproject.lifecyclemanagement.api.LifeCyclePolicy;
import org.opencastproject.lifecyclemanagement.api.LifeCyclePolicyAccessControlEntry;
import org.opencastproject.lifecyclemanagement.api.LifeCycleService;
import org.opencastproject.lifecyclemanagement.api.LifeCycleServiceException;
import org.opencastproject.lifecyclemanagement.impl.LifeCyclePolicyAccessControlEntryImpl;
import org.opencastproject.lifecyclemanagement.impl.LifeCyclePolicyImpl;
import org.opencastproject.security.api.DefaultOrganization;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.api.User;
import org.opencastproject.security.util.SecurityUtil;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.ReadinessIndicator;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import org.apache.felix.fileinstall.ArtifactInstaller;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
 * Loads, unloads, and reloads {@link LifeCyclePolicy}s from "*policy.yml" files in any of fileinstall's watch
 * directories.
 */
@Component(
    property = {
        "service.description=LifeCycle Policy Scanner"
    },
    immediate = true,
    service = { ArtifactInstaller.class, LifeCyclePolicyScanner.class }
)
public class LifeCyclePolicyScanner implements ArtifactInstaller {

  private static final Logger logger = LoggerFactory.getLogger(LifeCyclePolicyScanner.class);

  private ObjectMapper mapper;

  /** An internal collection of artifact id, bind the policy definition files and their id */
  protected Map<File, String> artifactIds = new HashMap<>();

  /** List of artifact parsed with error */
  protected final List<File> artifactsWithError = new ArrayList<>();

  /** OSGi bundle context */
  private BundleContext bundleCtx = null;

  /** Tag to define if the policies definition has already been loaded */
  private boolean isLPSinitialized = false;

  private LifeCycleService lifeCycleService;
  protected SecurityService securityService;
  protected OrganizationDirectoryService organizationDirectoryService;

  private PolicyFileNameFilter policyFilenameFilter;

  private User systemAdminUser;
  private Organization defaultOrganization;

  @Reference
  protected void addLifeCycleService(LifeCycleService service) {
    lifeCycleService = service;
  }

  @Reference
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  @Reference
  public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
    this.organizationDirectoryService = organizationDirectoryService;
  }

  @Activate
  void activate(BundleContext ctx) {
    this.bundleCtx = ctx;
    this.defaultOrganization = new DefaultOrganization();
    String systemAdminUserName = ctx.getProperty(SecurityUtil.PROPERTY_KEY_SYS_USER);
    this.systemAdminUser = SecurityUtil.createSystemUser(systemAdminUserName, defaultOrganization);

    this.policyFilenameFilter = new PolicyFileNameFilter("lifecyclepolicies", ".*\\.(yaml|yml)$");

    ObjectMapper om = new ObjectMapper(new YAMLFactory());
    SimpleModule sm = new SimpleModule();
    sm.addAbstractTypeMapping(LifeCyclePolicyAccessControlEntry.class, LifeCyclePolicyAccessControlEntryImpl.class);
    om.registerModule(sm);
    mapper = om;

    for (Organization org : organizationDirectoryService.getOrganizations()) {
      try {
        lifeCycleService.deleteAllLifeCyclePoliciesCreatedByConfig(org.getId());
      } catch (LifeCycleServiceException e) {
        logger.warn("Could not delete policies created from config for organization '{}'", org.getId(), e);
      }
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see org.apache.felix.fileinstall.ArtifactInstaller#install(java.io.File)
   */
  public void install(File artifact) {
    synchronized (artifactsWithError) {
      LifeCyclePolicyImpl policy = parseLifeCyclePolicyFile(artifact);
      if (policy == null) {
        logger.warn("Unable to install policy from '{}'", artifact.getName());
        artifactsWithError.add(artifact);
      } else {
        installLifeCyclePolicy(artifact, policy);
      }

      // Determine the number of available profiles
      String[] filesInDirectory = artifact.getParentFile().list(policyFilenameFilter);
      if (filesInDirectory == null) {
        throw new RuntimeException("error retrieving files from directory \"" + artifact.getParentFile() + "\"");
      }

      // Once all profiles have been loaded, announce readiness
      if ((filesInDirectory.length - artifactsWithError.size()) == artifactIds.size() && !isLPSinitialized) {
        logger.info("{} LifeCycle policies loaded", filesInDirectory.length - artifactsWithError.size());
        Dictionary<String, String> properties = new Hashtable<>();
        properties.put(ARTIFACT, "lifecyclepolicy");
        logger.debug("Indicating readiness of lifecycle policies");
        bundleCtx.registerService(ReadinessIndicator.class.getName(), new ReadinessIndicator(), properties);
        isLPSinitialized = true;
      }
    }
  }

  private void installLifeCyclePolicy(File artifact, LifeCyclePolicyImpl policy) {
    synchronized (artifactsWithError) {
      // Is there a policy with the exact same ID, but a different file name? Then ignore.
      final String policyIdentifier = policy.getId();
      for (Map.Entry<File, String> fileWithIdentifier : artifactIds.entrySet()) {
        if (fileWithIdentifier.getValue().equals(policyIdentifier) && !fileWithIdentifier.getKey().equals(artifact)) {
          logger.warn("Policy with identifier '{}' already registered in file '{}', ignoring", policyIdentifier,
              fileWithIdentifier.getKey());
          artifactsWithError.add(artifact);
          return;
        }
      }

      logger.debug("Installing policy from file '{}'", artifact.getName());
      artifactsWithError.remove(artifact);
      artifactIds.put(artifact, policyIdentifier);
      putLifeCyclePolicy(policy);

      logger.info("LifeCycle policy '{}' from file '{}' installed", policyIdentifier, artifact.getName());
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see org.apache.felix.fileinstall.ArtifactInstaller#uninstall(java.io.File)
   */
  public void uninstall(File artifact) {
    // Since the artifact is gone, we can't open it to read its ID. So we look in the local map.
    String identifier = artifactIds.remove(artifact);
    if (identifier != null) {
      removeLifeCyclePolicy(identifier);
      logger.info("Uninstalling lifecycle policy '{}' from file '{}'", identifier, artifact.getName());
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see org.apache.felix.fileinstall.ArtifactInstaller#update(java.io.File)
   */
  public void update(File artifact) {
    LifeCyclePolicyImpl policy = parseLifeCyclePolicyFile(artifact);

    if (policy != null) {
      uninstall(artifact);
      installLifeCyclePolicy(artifact, policy);
    }
  }

  /**
   * Parse the given lifecycle policy file and return the related lifecycle policy
   *
   * @param artifact
   *          The lifecycle policy file to parse
   * @return the lifecycle policy if the given contained a valid one, or null if the file can not be parsed.
   */
  public LifeCyclePolicyImpl parseLifeCyclePolicyFile(File artifact) {
    try (InputStream stream = new FileInputStream(artifact)) {
      LifeCyclePolicyImpl policy;

      policy = mapper.readValue(stream, LifeCyclePolicyImpl.class);
      policy.setCreatedFromConfig(true);

      if (!lifeCycleService.checkValidity(policy)) {
        throw new RuntimeException("Invalid policy " + policy.getId() + "");
      }

      return policy;
    } catch (Exception e) {
      logger.warn("Unable to parse policy from file '{}', {}", artifact.getName(), e.getMessage());
      return null;
    }
  }

  /**
   * Add the given lifecycle policy to the installed lifecycle policy id.
   *
   * @param policy
   *          the lifecycle policy
   */
  public void putLifeCyclePolicy(LifeCyclePolicyImpl policy) {
    SecurityUtil.runAs(securityService, defaultOrganization, systemAdminUser, () -> {
      try {
        lifeCycleService.getLifeCyclePolicyById(policy.getId(), policy.getOrganization());
        lifeCycleService.updateLifeCyclePolicy(policy);
      } catch (NotFoundException e) {
        try {
          lifeCycleService.createLifeCyclePolicy(policy);
        } catch (UnauthorizedException ex) {
          logger.error("System admin user not authorized to add lifecycle policy to database. By all accounts "
              + "this should not have happened");
          throw new RuntimeException(ex);
        } catch (LifeCycleServiceException ex) {
          logger.error("Could not add lifecycle policy to database", ex);
          throw new RuntimeException(ex);
        }
      } catch (UnauthorizedException e) {
        logger.error("System admin user not authorized to add lifecycle policy to database. By all accounts "
            + "this should not have happened");
        throw new RuntimeException(e);
      } catch (LifeCycleServiceException e) {
        logger.error("Could not add lifecycle policy to database", e);
        throw new RuntimeException(e);
      }
    });
  }

  /**
   * Remove the lifecycle policy with the given id from the installed definition list.
   *
   * @param identifier
   *          the lifecycle policy identifier
   */
  public void removeLifeCyclePolicy(String identifier) {
    SecurityUtil.runAs(securityService, defaultOrganization, systemAdminUser, () -> {
      try {
        lifeCycleService.deleteLifeCyclePolicy(identifier);
      } catch (NotFoundException e) {
        logger.warn("Attempted to delete life cycle policy with id " + identifier + " from datbaase, but policy "
            + "was not found.");
      } catch (UnauthorizedException e) {
        logger.error("System admin user not authorized to add lifecycle policy to database. By all accounts "
            + "this should not have happened");
        throw new RuntimeException(e);
      } catch (LifeCycleServiceException e) {
        logger.error("Could not delete lifecycle policy with id " + identifier + " from database", e);
        throw new RuntimeException(e);
      }
    });
  }

  /**
   * {@inheritDoc}
   *
   * @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File)
   */
  public boolean canHandle(File artifact) {
    return policyFilenameFilter.accept(artifact.getParentFile(), artifact.getName());
  }
}