/** * 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.ingest.scanner; import static org.opencastproject.security.util.SecurityUtil.getUserAndOrganization; import static org.opencastproject.util.data.Collections.dict; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.option; import static org.opencastproject.util.data.Option.some; import static org.opencastproject.util.data.Tuple.tuple; import org.opencastproject.ingest.api.IngestService; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.User; import org.opencastproject.security.api.UserDirectoryService; import org.opencastproject.security.util.SecurityContext; import org.opencastproject.util.data.Effect; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; import org.opencastproject.util.data.functions.Strings; import org.opencastproject.workingfilerepository.api.WorkingFileRepository; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.felix.fileinstall.ArtifactInstaller; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; /** * The inbox scanner monitors a directory for incoming media packages. * <p> * There is one InboxScanner instance per inbox. Each instance is configured by a config file in * <code>.../etc/load</code> named <code><inbox-scanned-pid>-<name>.cfg</code> where <code>name</code> * can be arbitrarily chosen and has no further meaning. <code>inbox-scanned-pid</code> must confirm to the PID given to * the InboxScanner in the declarative service (DS) configuration <code>OSGI-INF/inbox-scanner-service.xml</code>. * * <h3>Implementation notes</h3> * Monitoring leverages Apache FileInstall by implementing {@link ArtifactInstaller}. * * @see Ingestor */ public class InboxScannerService implements ArtifactInstaller, ManagedService { /** The logger */ private static final Logger logger = LoggerFactory.getLogger(InboxScannerService.class); /** The configuration key to use for determining the user to run as for ingest */ public static final String USER_NAME = "user.name"; /** The configuration key to use for determining the user's organization */ public static final String USER_ORG = "user.organization"; /** The configuration key to use for determining the workflow definition to use for ingest */ public static final String WORKFLOW_DEFINITION = "workflow.definition"; /** The configuration key to use for determining the default media flavor */ public static final String MEDIA_FLAVOR = "media.flavor"; /** The configuration key to use for determining the workflow configuration to use for ingest */ public static final String WORKFLOW_CONFIG = "workflow.config"; /** The configuration key to use for determining the inbox path */ public static final String INBOX_PATH = "inbox.path"; /** The configuration key to use for determining the polling interval in ms. */ public static final String INBOX_POLL = "inbox.poll"; private IngestService ingestService; private WorkingFileRepository workingFileRepository; private SecurityService securityService; private UserDirectoryService userDir; private OrganizationDirectoryService orgDir; private ComponentContext cc; private volatile Option<Ingestor> ingestor = none(); private volatile Option<Configuration> fileInstallCfg = none(); /** OSGi callback. */ // synchronized with updated(Dictionary) public synchronized void activate(ComponentContext cc) { this.cc = cc; } /** OSGi callback. */ public void deactivate() { fileInstallCfg.foreach(removeFileInstallCfg); } // synchronized with activate(ComponentContext) @Override public synchronized void updated(Dictionary properties) throws ConfigurationException { // build scanner configuration final String orgId = getCfg(properties, USER_ORG); final String userId = getCfg(properties, USER_NAME); final String mediaFlavor = getCfg(properties, MEDIA_FLAVOR); final String workflowDefinition = getCfg(properties, WORKFLOW_DEFINITION); final Map<String, String> workflowConfig = getCfgAsMap(properties, WORKFLOW_CONFIG); final int interval = getCfgAsInt(properties, INBOX_POLL); final File inbox = new File(getCfg(properties, INBOX_PATH)); if (!inbox.isDirectory()) { try { FileUtils.forceMkdir(inbox); } catch (IOException e) { throw new ConfigurationException(INBOX_PATH, "%s does not exists and could not be created".format(inbox.getAbsolutePath())); } } /* We need to be able to read from the inbox to get files from there */ if (!inbox.canRead()) { throw new ConfigurationException(INBOX_PATH, "Cannot read from %s".format(inbox.getAbsolutePath())); } /* We need to be able to write to the inbox to remove files after they have been ingested */ if (!inbox.canWrite()) { throw new ConfigurationException(INBOX_PATH, "Cannot write to %s".format(inbox.getAbsolutePath())); } final int maxthreads = option(cc.getBundleContext().getProperty("org.opencastproject.inbox.threads")).bind( Strings.toInt).getOrElse(1); final Option<SecurityContext> secCtx = getUserAndOrganization(securityService, orgDir, orgId, userDir, userId) .bind(new Function<Tuple<User, Organization>, Option<SecurityContext>>() { @Override public Option<SecurityContext> apply(Tuple<User, Organization> a) { return some(new SecurityContext(securityService, a.getB(), a.getA())); } }); // Only setup new inbox if security context could be aquired if (secCtx.isSome()) { // remove old file install configuration fileInstallCfg.foreach(removeFileInstallCfg); // set up new file install config fileInstallCfg = some(configureFileInstall(cc.getBundleContext(), inbox, interval)); // create new scanner ingestor = some(new Ingestor(ingestService, workingFileRepository, secCtx.get(), workflowDefinition, workflowConfig, mediaFlavor, inbox, maxthreads)); logger.info("Now watching inbox {}", inbox.getAbsolutePath()); } else { logger.warn("Cannot create security context for user {}, organization {}. " + "Either the organization or the user does not exist", userId, orgId); } } public static final Effect<Configuration> removeFileInstallCfg = new Effect.X<Configuration>() { @Override protected void xrun(Configuration cfg) throws Exception { cfg.delete(); } }; /** * Setup an Apache FileInstall configuration for the inbox folder this scanner is responsible for. * * see section 104.4.1 Location Binding, paragraph 4, of the OSGi Spec 4.2 The correct permissions are needed in order * to set configuration data for a bundle other than the calling bundle itself. */ public static Configuration configureFileInstall(BundleContext bc, File inbox, int interval) { final ServiceReference caRef = bc.getServiceReference(ConfigurationAdmin.class.getName()); if (caRef == null) { throw new Error("Cannot obtain a reference to the ConfigurationAdmin service"); } final Dictionary<String, String> fileInstallConfig = dict(tuple("felix.fileinstall.dir", inbox.getAbsolutePath()), tuple("felix.fileinstall.poll", Integer.toString(interval))); // update file install config with the new directory try { final String fileInstallBundleLocation = bc.getServiceReferences("org.osgi.service.cm.ManagedServiceFactory", "(service.pid=org.apache.felix.fileinstall)")[0].getBundle().getLocation(); final Configuration conf = ((ConfigurationAdmin) bc.getService(caRef)).createFactoryConfiguration( "org.apache.felix.fileinstall", fileInstallBundleLocation); conf.update(fileInstallConfig); return conf; } catch (Exception e) { throw new Error(e); } } // -- // FileInstall callback, called on a different thread // Attention: This method may be called _before_ the updated(Dictionary) which means that config parameters // are not set yet. @Override public boolean canHandle(final File artifact) { return ingestor.fmap(new Function<Ingestor, Boolean>() { @Override public Boolean apply(Ingestor ingestor) { return ingestor.canHandle(artifact); } }).getOrElse(false); } @Override public void install(final File artifact) throws Exception { ingestor.foreach(new Effect<Ingestor>() { @Override protected void run(Ingestor ingestor) { ingestor.ingest(artifact); } }); } @Override public void update(File artifact) throws Exception { // To change body of implemented methods use File | Settings | File Templates. } @Override public void uninstall(File artifact) throws Exception { // To change body of implemented methods use File | Settings | File Templates. } // -- /** OSGi callback to set the ingest service. */ public void setIngestService(IngestService ingestService) { this.ingestService = ingestService; } /** OSGi callback to set the workspace */ public void setWorkingFileRepository(WorkingFileRepository workingFileRepository) { this.workingFileRepository = workingFileRepository; } /** OSGi callback to set the security service. */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi callback to set the user directory. */ public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDir = userDirectoryService; } /** OSGi callback to set the organization directory server. */ public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) { this.orgDir = organizationDirectoryService; } /** * Get a mandatory, non-blank value from a dictionary. * * @throws ConfigurationException * key does not exist or its value is blank */ public static String getCfg(Dictionary d, String key) throws ConfigurationException { Object p = d.get(key); if (p == null) throw new ConfigurationException(key, "does not exist"); String ps = p.toString(); if (StringUtils.isBlank(ps)) throw new ConfigurationException(key, "is blank"); return ps; } public static Map<String, String> getCfgAsMap(Dictionary<String, String> d, String key) throws ConfigurationException { HashMap<String, String> config = new HashMap<String, String>(); for (Enumeration<String> e = d.keys(); e.hasMoreElements();) { String dKey = (String) e.nextElement(); if (dKey.startsWith(key)) config.put(dKey.substring(key.length() + 1), (String) d.get(dKey)); } return config; } /** * Get a mandatory integer from a dictionary. * * @throws ConfigurationException * key does not exist or is not an integer */ public static int getCfgAsInt(Dictionary d, String key) throws ConfigurationException { try { return Integer.parseInt(getCfg(d, key)); } catch (NumberFormatException e) { throw new ConfigurationException(key, "not an integer"); } } }