All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.series.impl.solr.SeriesServiceSolrIndex Maven / Gradle / Ivy

/**
 * 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.series.impl.solr;

import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
import static org.opencastproject.util.data.Option.option;

import org.opencastproject.metadata.dublincore.DCMIPeriod;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogList;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
import org.opencastproject.metadata.dublincore.Temporal;
import org.opencastproject.security.api.AccessControlEntry;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AccessControlParser;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.Permissions;
import org.opencastproject.security.api.Role;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesQuery;
import org.opencastproject.series.impl.SeriesServiceDatabaseException;
import org.opencastproject.series.impl.SeriesServiceIndex;
import org.opencastproject.solr.SolrServerFactory;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.SolrUtils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Implements {@link SeriesServiceIndex}.
 */
@Component(
  immediate = true,
  service = { SeriesServiceIndex.class }
)
public class SeriesServiceSolrIndex implements SeriesServiceIndex {

  /** Configuration key for a remote solr server */
  public static final String CONFIG_SOLR_URL = "org.opencastproject.series.solr.url";

  /** Configuration key for an embedded solr configuration and data directory */
  public static final String CONFIG_SOLR_ROOT = "org.opencastproject.series.solr.dir";

  /** Delimeter used for concatenating multivalued fields for sorting fields in solr */
  public static final String SOLR_MULTIVALUED_DELIMETER = "; ";

  /** Date format supported by solr */
  public static final String SOLR_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z";

  /** The logger */
  protected static final Logger logger = LoggerFactory.getLogger(SeriesServiceSolrIndex.class);

  /** Connection to the solr server. Solr is used to search for workflows. The workflow data are stored as xml files. */
  protected SolrServer solrServer = null;

  /** The root directory to use for solr config and data files */
  protected String solrRoot = null;

  /** The URL to connect to a remote solr server */
  protected URL solrServerUrl = null;

  /** Dublin core service */
  protected DublinCoreCatalogService dcService;

  /** The security service */
  protected SecurityService securityService;

  /** Whether indexing is synchronous or asynchronous */
  protected boolean synchronousIndexing;

  /** Executor used for asynchronous indexing */
  protected ExecutorService indexingExecutor;

  /**
   * No-argument constructor for OSGi declarative services.
   */
  public SeriesServiceSolrIndex() {
  }

  /**
   * No-argument constructor for OSGi declarative services.
   */
  public SeriesServiceSolrIndex(String storageDirectory) {
    solrRoot = storageDirectory;
  }

  /**
   * OSGi callback for setting Dublin core service.
   *
   * @param dcService
   *          {@link DublinCoreCatalogService} object
   */
  @Reference(name = "dc")
  public void setDublinCoreService(DublinCoreCatalogService dcService) {
    this.dcService = dcService;
  }

  /**
   * OSGi callback for setting Dublin core service.
   *
   * @param securityService
   *          the securityService to set
   */
  @Reference(name = "security-service")
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  /**
   * Callback from the OSGi environment on component registration. Retrieves location of the solr index.
   *
   * @param cc
   *          the component context
   */
  @Activate
  public void activate(ComponentContext cc) {

    if (cc == null) {
      if (solrRoot == null)
        throw new IllegalStateException("Storage dir must be set");
      // default to synchronous indexing
      synchronousIndexing = true;
    } else {
      String solrServerUrlConfig = StringUtils.trimToNull(cc.getBundleContext().getProperty(CONFIG_SOLR_URL));
      if (solrServerUrlConfig != null) {
        try {
          solrServerUrl = new URL(solrServerUrlConfig);
        } catch (MalformedURLException e) {
          throw new IllegalStateException("Unable to connect to solr at " + solrServerUrlConfig, e);
        }
      } else {
        solrRoot = SolrServerFactory.getEmbeddedDir(cc, CONFIG_SOLR_ROOT, "series");
      }

      Object syncIndexingConfig = cc.getProperties().get("synchronousIndexing");
      synchronousIndexing = (syncIndexingConfig == null) || !(syncIndexingConfig instanceof Boolean)
              || (Boolean) syncIndexingConfig;
    }

    activate();
  }

  /**
   * OSGi callback for deactivation.
   *
   * @param cc
   *          the component context
   */
  @Deactivate
  public void deactivate(ComponentContext cc) {
    deactivate();
  }

  @Override
  public void activate() {
    // Set up the solr server
    if (solrServerUrl != null) {
      solrServer = SolrServerFactory.newRemoteInstance(solrServerUrl);
    } else {
      try {
        setupSolr(new File(solrRoot));
      } catch (IOException | SolrServerException e) {
        throw new IllegalStateException("Unable to connect to solr at " + solrRoot, e);
      }
    }

    // set up indexing
    if (this.synchronousIndexing) {
      logger.debug("Series will be added to the search index synchronously");
    } else {
      logger.debug("Series will be added to the search index asynchronously");
      indexingExecutor = Executors.newSingleThreadExecutor();
    }
  }

  /**
   * Prepares the embedded solr environment.
   *
   * @param solrRoot
   *          the solr root directory
   */
  public void setupSolr(File solrRoot) throws IOException, SolrServerException {
    logger.debug("Setting up solr search index at {}", solrRoot);
    File solrConfigDir = new File(solrRoot, "conf");

    // Create the config directory
    if (solrConfigDir.exists()) {
      logger.debug("solr search index found at {}", solrConfigDir);
    } else {
      logger.debug("solr config directory doesn't exist.  Creating {}", solrConfigDir);
      FileUtils.forceMkdir(solrConfigDir);
    }

    // Make sure there is a configuration in place
    copyClasspathResourceToFile("/solr/conf/protwords.txt", solrConfigDir);
    copyClasspathResourceToFile("/solr/conf/schema.xml", solrConfigDir);
    copyClasspathResourceToFile("/solr/conf/scripts.conf", solrConfigDir);
    copyClasspathResourceToFile("/solr/conf/solrconfig.xml", solrConfigDir);
    copyClasspathResourceToFile("/solr/conf/stopwords.txt", solrConfigDir);
    copyClasspathResourceToFile("/solr/conf/synonyms.txt", solrConfigDir);

    // Test for the existence of a data directory
    File solrDataDir = new File(solrRoot, "data");
    if (!solrDataDir.exists()) {
      FileUtils.forceMkdir(solrDataDir);
    }

    // Test for the existence of the index. Note that an empty index directory will prevent solr from
    // completing normal setup.
    File solrIndexDir = new File(solrDataDir, "index");
    if (solrIndexDir.isDirectory() && solrIndexDir.list().length == 0) {
      FileUtils.deleteDirectory(solrIndexDir);
    }

    solrServer = SolrServerFactory.newEmbeddedInstance(solrRoot, solrDataDir);
  }

  @Override
  public void deactivate() {
    SolrServerFactory.shutdown(solrServer);
  }

  // TODO: generalize this method
  private void copyClasspathResourceToFile(String classpath, File dir) {
    InputStream in = null;
    FileOutputStream fos = null;
    try {
      in = SeriesServiceSolrIndex.class.getResourceAsStream(classpath);
      File file = new File(dir, FilenameUtils.getName(classpath));
      logger.debug("copying " + classpath + " to " + file);
      fos = new FileOutputStream(file);
      IOUtils.copy(in, fos);
    } catch (IOException e) {
      throw new RuntimeException("Error copying solr classpath resource to the filesystem", e);
    } finally {
      IOUtils.closeQuietly(in);
      IOUtils.closeQuietly(fos);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * org.opencastproject.series.impl.SeriesServiceIndex#index(org.opencastproject.metadata.dublincore.DublinCoreCatalog)
   */
  @Override
  public void updateIndex(DublinCoreCatalog dc) throws SeriesServiceDatabaseException {

    final SolrInputDocument doc = createDocument(dc);

    if (synchronousIndexing) {
      try {
        synchronized (solrServer) {
          solrServer.add(doc);
          solrServer.commit();
        }
      } catch (Exception e) {
        throw new SeriesServiceDatabaseException("Unable to index series", e);
      }
    } else {
      indexingExecutor.submit(new Runnable() {
        @Override
        public void run() {
          try {
            synchronized (solrServer) {
              solrServer.add(doc);
              solrServer.commit();
            }
          } catch (Exception e) {
            logger.warn("Unable to index series {}: {}", doc.getFieldValue(SolrFields.COMPOSITE_ID_KEY),
                    e.getMessage());
          }
        }
      });
    }
  }

  @Override
  public void updateSecurityPolicy(String seriesId, AccessControlList accessControl)
          throws NotFoundException, SeriesServiceDatabaseException {
    if (accessControl == null) {
      logger.warn("Access control parameter is null: skipping update for series '{}'", seriesId);
      return;
    }
    SolrDocument seriesDoc = getSolrDocumentByID(seriesId);
    if (seriesDoc == null) {
      logger.debug("No series with ID " + seriesId + " found.");
      throw new NotFoundException("Series with ID " + seriesId + " was not found.");
    }
    String serializedAC;
    try {
      serializedAC = AccessControlParser.toXml(accessControl);
    } catch (Exception e) {
      logger.error("Could not parse access control parameter: {}", e.getMessage());
      throw new SeriesServiceDatabaseException(e);
    }

    final SolrInputDocument inputDoc = ClientUtils.toSolrInputDocument(seriesDoc);
    inputDoc.setField(SolrFields.ACCESS_CONTROL_KEY, serializedAC);
    inputDoc.removeField(SolrFields.ACCESS_CONTROL_CONTRIBUTE);
    inputDoc.removeField(SolrFields.ACCESS_CONTROL_EDIT);
    inputDoc.removeField(SolrFields.ACCESS_CONTROL_READ);
    for (AccessControlEntry ace : accessControl.getEntries()) {
      if (Permissions.Action.CONTRIBUTE.toString().equals(ace.getAction()) && ace.isAllow()) {
        inputDoc.addField(SolrFields.ACCESS_CONTROL_CONTRIBUTE, ace.getRole());
      } else if (Permissions.Action.WRITE.toString().equals(ace.getAction()) && ace.isAllow()) {
        inputDoc.addField(SolrFields.ACCESS_CONTROL_EDIT, ace.getRole());
      } else if (Permissions.Action.READ.toString().equals(ace.getAction()) && ace.isAllow()) {
        inputDoc.addField(SolrFields.ACCESS_CONTROL_READ, ace.getRole());
      }
    }

    if (synchronousIndexing) {
      try {
        synchronized (solrServer) {
          solrServer.add(inputDoc);
          solrServer.commit();
        }
      } catch (Exception e) {
        throw new SeriesServiceDatabaseException("Unable to index ACL", e);
      }
    } else {
      indexingExecutor.submit(new Runnable() {
        @Override
        public void run() {
          try {
            synchronized (solrServer) {
              solrServer.add(inputDoc);
              solrServer.commit();
            }
          } catch (Exception e) {
            logger.warn("Unable to index ACL for series {}: {}", inputDoc.getFieldValue(SolrFields.COMPOSITE_ID_KEY),
                    e.getMessage());
          }
        }
      });
    }
  }

  /**
   * Creates solr document for inserting into solr index.
   *
   * @param dc
   *          {@link DublinCoreCatalog} to be stored in index
   * @return {@link SolrInputDocument} created out of Dublin core
   */
  protected SolrInputDocument createDocument(DublinCoreCatalog dc) {
    final SolrInputDocument doc = new SolrInputDocument();
    String dublinCoreId = dc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
    String orgId = securityService.getOrganization().getId();
    doc.addField(SolrFields.COMPOSITE_ID_KEY, getCompositeKey(dublinCoreId, orgId));
    doc.addField(SolrFields.ORGANIZATION, orgId);
    doc.addField(SolrFields.IDENTIFIER_KEY, dublinCoreId);
    try {
      doc.addField(SolrFields.XML_KEY, serializeDublinCore(dc));
    } catch (IOException e1) {
      throw new IllegalArgumentException(e1);
    }
    doc.addField(SolrFields.OPT_OUT, false);

    // single valued fields
    if (dc.hasValue(DublinCore.PROPERTY_TITLE)) {
      doc.addField(SolrFields.TITLE_KEY, dc.getFirst(DublinCore.PROPERTY_TITLE));
      doc.addField(SolrFields.TITLE_KEY + "_sort", dc.getFirst(DublinCore.PROPERTY_TITLE));
    }
    if (dc.hasValue(DublinCore.PROPERTY_CREATED)) {
      final Temporal temporal = EncodingSchemeUtils.decodeTemporal(dc.get(DublinCore.PROPERTY_CREATED).get(0));
      temporal.fold(new Temporal.Match() {
        @Override
        public Void period(DCMIPeriod period) {
          doc.addField(SolrFields.CREATED_KEY, period.getStart());
          return null;
        }

        @Override
        public Void instant(Date instant) {
          doc.addField(SolrFields.CREATED_KEY, instant);
          return null;
        }

        @Override
        public Void duration(long duration) {
          throw new IllegalArgumentException("Dublin core dc:created is neither a date nor a period");
        }
      });
    }
    if (dc.hasValue(DublinCore.PROPERTY_AVAILABLE)) {
      Temporal temporal = EncodingSchemeUtils.decodeTemporal(dc.get(DublinCore.PROPERTY_AVAILABLE).get(0));
      temporal.fold(new Temporal.Match() {
        @Override
        public Void period(DCMIPeriod period) {
          if (period.hasStart()) {
            doc.addField(SolrFields.AVAILABLE_FROM_KEY, period.getStart());
          }
          if (period.hasEnd()) {
            doc.addField(SolrFields.AVAILABLE_TO_KEY, period.getEnd());
          }
          return null;
        }

        @Override
        public Void instant(Date instant) {
          doc.addField(SolrFields.AVAILABLE_FROM_KEY, instant);
          return null;
        }

        @Override
        public Void duration(long duration) {
          throw new IllegalArgumentException("Dublin core field dc:available is neither a date nor a period");
        }
      });
    }

    // multivalued fields
    addMultiValuedFieldToSolrDocument(doc, SolrFields.SUBJECT_KEY, dc.get(DublinCore.PROPERTY_SUBJECT));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.CREATOR_KEY, dc.get(DublinCore.PROPERTY_CREATOR));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.PUBLISHER_KEY, dc.get(DublinCore.PROPERTY_PUBLISHER));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.CONTRIBUTOR_KEY, dc.get(DublinCore.PROPERTY_CONTRIBUTOR));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.ABSTRACT_KEY, dc.get(DublinCore.PROPERTY_ABSTRACT));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.DESCRIPTION_KEY, dc.get(DublinCore.PROPERTY_DESCRIPTION));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.LANGUAGE_KEY, dc.get(DublinCore.PROPERTY_LANGUAGE));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.RIGHTS_HOLDER_KEY, dc.get(DublinCore.PROPERTY_RIGHTS_HOLDER));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.SPATIAL_KEY, dc.get(DublinCore.PROPERTY_SPATIAL));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.TEMPORAL_KEY, dc.get(DublinCore.PROPERTY_TEMPORAL));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.IS_PART_OF_KEY, dc.get(DublinCore.PROPERTY_IS_PART_OF));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.REPLACES_KEY, dc.get(DublinCore.PROPERTY_REPLACES));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.TYPE_KEY, dc.get(DublinCore.PROPERTY_TYPE));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.ACCESS_RIGHTS_KEY, dc.get(DublinCore.PROPERTY_ACCESS_RIGHTS));
    addMultiValuedFieldToSolrDocument(doc, SolrFields.LICENSE_KEY, dc.get(DublinCore.PROPERTY_LICENSE));

    return doc;
  }

  /**
   * Builds a composite key for use in solr.
   *
   * @param dublinCoreId
   *          the DC identifier, which must be unique for an organization
   * @param orgId
   *          the organization identifier
   * @return the composite key, or null if either dublinCoreId or orgId are empty
   */
  protected String getCompositeKey(String dublinCoreId, String orgId) {
    if (StringUtils.isEmpty(dublinCoreId) || StringUtils.isEmpty(orgId)) {
      logger.debug("can not create a composite key without values for series and organization IDs");
      return null;
    } else {
      return new StringBuilder(orgId).append("_").append(dublinCoreId).toString();
    }
  }

  /**
   * Parse a series identifier from a composite key and organization identifier
   *
   * @param compositeId
   *          the composite identifier
   * @param orgId
   *          the organization identifier
   *
   * @return series identifier, or null if either the compositeId or orgId are empty
   *          or the compositeId doesn't start with orgId
   */
  protected String getSeriesIDfromCompositeID(String compositeId, String orgId) {
    if (StringUtils.isEmpty(compositeId) || StringUtils.isEmpty(orgId) || !compositeId.startsWith(orgId)) {
      logger.debug("can not parse series Id from a solr entity composite Id");
      return null;
    } else {
      if (compositeId.length() <= (orgId.length() + 1)) {
        logger.debug("composite Id does not contain a organization Id");
        return null;
      }

      return StringUtils.substring(compositeId, orgId.length() + 1);
    }
  }

  /**
   * Add field to solr document that can contain multiple values. For sorting field, those values are concatenated and
   * multivalued field delimiter is used.
   *
   * @param doc
   *          {@link SolrInputDocument} for fields to be added to
   * @param solrField
   *          name of the solr field to add value. For sorting field "_sort" is appended
   * @param dcValues
   *          List of Dublin core values to be added to solr document
   */
  private void addMultiValuedFieldToSolrDocument(SolrInputDocument doc, String solrField,
          List dcValues) {
    if (!dcValues.isEmpty()) {
      List values = new LinkedList();
      StringBuilder builder = new StringBuilder();
      values.add(dcValues.get(0).getValue());
      builder.append(dcValues.get(0).getValue());
      for (int i = 1; i < dcValues.size(); i++) {
        values.add(dcValues.get(i).getValue());
        builder.append(SOLR_MULTIVALUED_DELIMETER);
        builder.append(dcValues.get(i).getValue());
      }
      doc.addField(solrField, values);
      doc.addField(solrField + "_sort", builder.toString());
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.opencastproject.series.impl.SeriesServiceIndex#count()
   */
  @Override
  public long count() throws SeriesServiceDatabaseException {
    try {
      QueryResponse response = solrServer.query(new SolrQuery("*:*"));
      return response.getResults().getNumFound();
    } catch (SolrServerException e) {
      throw new SeriesServiceDatabaseException(e);
    }
  }

  /**
   * Appends query parameters to a solr query
   *
   * @param sb
   *          The {@link StringBuilder} containing the query
   * @param key
   *          the key for this search parameter
   * @param value
   *          the value for this search parameter
   * @return the appended {@link StringBuilder}
   */
  private StringBuilder appendAnd(StringBuilder sb, String key, String value) {
    if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
      return sb;
    }
    if (sb.length() > 0) {
      sb.append(" AND ");
    }
    sb.append(key);
    sb.append(":");
    sb.append(ClientUtils.escapeQueryChars(value));
    return sb;
  }

  /**
   * Appends a multivalued query parameter to a solr query
   *
   * @param sb
   *          The {@link StringBuilder} containing the query
   * @param key
   *          the key for this search parameter
   * @param values
   *          the values for this search parameter
   * @return the appended {@link StringBuilder}
   */
  private StringBuilder appendAnd(StringBuilder sb, String key, String[] values) {
    return append(sb, "AND", key, values);
  }

  private StringBuilder append(StringBuilder sb, String bool, String key, String[] values) {
    if (StringUtils.isBlank(key) || values.length == 0) {
      return sb;
    }
    if (sb.length() > 0) {
      sb.append(" ").append(bool).append(" (");
    }
    for (int i = 0; i < values.length; i++) {
      if (i > 0) {
        sb.append(" OR ");
      }
      sb.append(key);
      sb.append(":");
      sb.append(ClientUtils.escapeQueryChars(values[i]));
    }
    sb.append(")");
    return sb;
  }

  /**
   * Appends query parameters to a solr query in a way that they are found even though they are not treated as a full
   * word in solr.
   *
   * @param sb
   *          The {@link StringBuilder} containing the query
   * @param key
   *          the key for this search parameter
   * @param value
   *          the value for this search parameter
   * @return the appended {@link StringBuilder}
   */
  private StringBuilder appendFuzzy(StringBuilder sb, String key, String value) {
    if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
      return sb;
    }
    if (sb.length() > 0) {
      sb.append(" AND ");
    }
    sb.append("(");
    sb.append(key).append(":").append(ClientUtils.escapeQueryChars(value));
    sb.append(" OR ");
    sb.append(key).append(":*").append(ClientUtils.escapeQueryChars(value)).append("*");
    sb.append(")");
    return sb;
  }

  /**
   * Appends query parameters to a solr query
   *
   * @param sb
   *          The {@link StringBuilder} containing the query
   * @param key
   *          the key for this search parameter
   * @return the appended {@link StringBuilder}
   */
  private StringBuilder appendAnd(StringBuilder sb, String key, Date startDate, Date endDate) {
    if (StringUtils.isBlank(key) || (startDate == null && endDate == null)) {
      return sb;
    }
    if (sb.length() > 0) {
      sb.append(" AND ");
    }
    if (startDate == null)
      startDate = new Date(0);
    if (endDate == null)
      endDate = new Date(Long.MAX_VALUE);
    sb.append(key);
    sb.append(":");
    sb.append(SolrUtils.serializeDateRange(option(startDate), option(endDate)));
    return sb;
  }

  /**
   * Builds a solr search query from a {@link SeriesQuery}.
   *
   * @param query
   *          the series query
   * @param forEdit
   *          if this query should return only series available to the current user for editing
   * @return the solr query string
   */
  protected String buildSolrQueryString(SeriesQuery query, boolean forEdit) {
    String orgId = securityService.getOrganization().getId();
    StringBuilder sb = new StringBuilder();
    // Restrict to exact match on Composite series Id Key when not searching with fuzzy match
    if (!query.isFuzzyMatch()) {
      appendAnd(sb, SolrFields.COMPOSITE_ID_KEY, getCompositeKey(query.getSeriesId(), orgId));
    }
    appendFuzzy(sb, SolrFields.IDENTIFIER_KEY, query.getSeriesId());
    appendFuzzy(sb, SolrFields.TITLE_KEY, query.getSeriesTitle());
    appendFuzzy(sb, SolrFields.FULLTEXT_KEY, query.getText());
    appendFuzzy(sb, SolrFields.CREATOR_KEY, query.getCreator());
    appendFuzzy(sb, SolrFields.CONTRIBUTOR_KEY, query.getContributor());
    appendAnd(sb, SolrFields.LANGUAGE_KEY, query.getLanguage());
    appendAnd(sb, SolrFields.LICENSE_KEY, query.getLicense());
    appendFuzzy(sb, SolrFields.SUBJECT_KEY, query.getSubject());
    appendFuzzy(sb, SolrFields.ABSTRACT_KEY, query.getAbstract());
    appendFuzzy(sb, SolrFields.DESCRIPTION_KEY, query.getDescription());
    appendFuzzy(sb, SolrFields.PUBLISHER_KEY, query.getPublisher());
    appendFuzzy(sb, SolrFields.RIGHTS_HOLDER_KEY, query.getRightsHolder());
    appendFuzzy(sb, SolrFields.SUBJECT_KEY, query.getSubject());
    appendAnd(sb, SolrFields.CREATED_KEY, query.getCreatedFrom(), query.getCreatedTo());
    appendAnd(sb, SolrFields.ORGANIZATION, orgId);

    appendAuthorization(sb, forEdit);

    return sb.toString();
  }

  /**
   * Appends the authorization information to the solr query string
   *
   * @param sb
   *          the {@link StringBuilder} containing the query
   * @param forEdit
   *          if this query should return only series available to the current user for editing
   *
   * @return the appended {@link StringBuilder}
   */
  protected StringBuilder appendAuthorization(StringBuilder sb, boolean forEdit) {
    User currentUser = securityService.getUser();
    Organization currentOrg = securityService.getOrganization();
    if (!currentUser.hasRole(currentOrg.getAdminRole()) && !currentUser.hasRole(GLOBAL_ADMIN_ROLE)) {
      List roleList = new ArrayList();
      for (Role role : currentUser.getRoles()) {
        roleList.add(role.getName());
      }
      String[] roles = roleList.toArray(new String[roleList.size()]);
      if (forEdit) {
        appendAnd(sb, SolrFields.ACCESS_CONTROL_EDIT, roles);
      } else if (roles.length > 0) {
        sb.append(" AND (");
        append(sb, "", SolrFields.ACCESS_CONTROL_CONTRIBUTE, roles);
        sb.append(" OR ");
        append(sb, "", SolrFields.ACCESS_CONTROL_READ, roles);
        sb.append(")");
      }
    }
    return sb;
  }

  /**
   * Returns the search index' field name that corresponds to the sort field.
   *
   * @param sort
   *          the sort field
   * @return the field name in the search index
   */
  protected String getSortField(SeriesQuery.Sort sort) {
    switch (sort) {
      case ABSTRACT:
        return SolrFields.ABSTRACT_KEY;
      case ACCESS:
        return SolrFields.ACCESS_RIGHTS_KEY;
      case AVAILABLE_FROM:
        return SolrFields.AVAILABLE_FROM_KEY;
      case AVAILABLE_TO:
        return SolrFields.AVAILABLE_TO_KEY;
      case CONTRIBUTOR:
        return SolrFields.CONTRIBUTOR_KEY;
      case CREATED:
        return SolrFields.CREATED_KEY;
      case CREATOR:
        return SolrFields.CREATOR_KEY;
      case DESCRIPTION:
        return SolrFields.DESCRIPTION_KEY;
      case IDENTIFIER:
        return SolrFields.IDENTIFIER_KEY;
      case IS_PART_OF:
        return SolrFields.IS_PART_OF_KEY;
      case LANGUAGE:
        return SolrFields.LANGUAGE_KEY;
      case LICENCE:
        return SolrFields.LICENSE_KEY;
      case PUBLISHER:
        return SolrFields.PUBLISHER_KEY;
      case REPLACES:
        return SolrFields.REPLACES_KEY;
      case RIGHTS_HOLDER:
        return SolrFields.RIGHTS_HOLDER_KEY;
      case SPATIAL:
        return SolrFields.SPATIAL_KEY;
      case SUBJECT:
        return SolrFields.SUBJECT_KEY;
      case TEMPORAL:
        return SolrFields.TEMPORAL_KEY;
      case TITLE:
        return SolrFields.TITLE_KEY;
      case TYPE:
        return SolrFields.TYPE_KEY;
      default:
        throw new IllegalArgumentException("No mapping found between sort field and index");
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public DublinCoreCatalogList search(SeriesQuery query) throws SeriesServiceDatabaseException {
    int count = query.getCount() > 0 ? query.getCount() : 20; // default to 20 items if not specified
    int startPage = query.getStartPage() > 0 ? query.getStartPage() : 0; // default to page zero

    SolrQuery solrQuery = new SolrQuery();
    solrQuery.setRows(count);
    solrQuery.setStart(startPage * count);

    String solrQueryString = null;
    solrQueryString = buildSolrQueryString(query, query.isEdit());
    solrQuery.setQuery(solrQueryString);

    if (query.getSort() != null) {
      SolrQuery.ORDER order = query.isSortAscending() ? SolrQuery.ORDER.asc : SolrQuery.ORDER.desc;
      solrQuery.addSortField(getSortField(query.getSort()) + "_sort", order);
    }

    if (!SeriesQuery.Sort.CREATED.equals(query.getSort())) {
      solrQuery.addSortField(getSortField(SeriesQuery.Sort.CREATED) + "_sort", SolrQuery.ORDER.desc);
    }

    List result;

    try {
      QueryResponse response = solrServer.query(solrQuery);
      SolrDocumentList items = response.getResults();

      result = new LinkedList();

      // Iterate through the results
      for (SolrDocument doc : items) {
        DublinCoreCatalog item = parseDublinCore((String) doc.get(SolrFields.XML_KEY));
        result.add(item);
      }
      return new DublinCoreCatalogList(result, response.getResults().getNumFound());
    } catch (Exception e) {
      logger.error("Could not retrieve results: {}", e.getMessage());
      throw new SeriesServiceDatabaseException(e);
    }
  }

  @Override
  public Map queryIdTitleMap() throws SeriesServiceDatabaseException {
    SolrQuery solrQuery = new SolrQuery();
    solrQuery.setStart(0);
    solrQuery.setRows(Integer.MAX_VALUE);
    solrQuery.setQuery(buildSolrQueryString(new SeriesQuery(), false));
    solrQuery.addSortField(getSortField(SeriesQuery.Sort.TITLE) + "_sort", SolrQuery.ORDER.asc);

    Map result;
    try {
      QueryResponse response = solrServer.query(solrQuery);
      SolrDocumentList items = response.getResults();
      result = new HashMap();
      for (SolrDocument doc : items) {
        String seriesId = getSeriesIDfromCompositeID((String) doc.get(SolrFields.COMPOSITE_ID_KEY),
                securityService.getOrganization().getId());
        String seriesTitle = (String) doc.get(SolrFields.TITLE_KEY);
        result.put(seriesId, seriesTitle);
      }
      return result;
    } catch (Exception e) {
      logger.error("Could not retrieve results: {}", e.getMessage());
      throw new SeriesServiceDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void delete(final String id) throws SeriesServiceDatabaseException {
    if (synchronousIndexing) {
      try {
        synchronized (solrServer) {
          solrServer.deleteById(getCompositeKey(id, securityService.getOrganization().getId()));
          solrServer.commit();
        }
      } catch (Exception e) {
        throw new SeriesServiceDatabaseException(e);
      }
    } else {
      indexingExecutor.submit(new Runnable() {
        @Override
        public void run() {
          try {
            synchronized (solrServer) {
              solrServer.deleteById(id);
              solrServer.commit();
            }
          } catch (Exception e) {
            logger.warn("Could not delete from index series {}: {}", id, e.getMessage());
          }
        }
      });
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.opencastproject.series.impl.SeriesServiceIndex#get(java.lang.String)
   */
  @Override
  public DublinCoreCatalog getDublinCore(String seriesId) throws SeriesServiceDatabaseException, NotFoundException {
    SolrDocument result = getSolrDocumentByID(seriesId);
    if (result == null) {
      logger.debug("No series exists with ID {}", seriesId);
      throw new NotFoundException("Series with ID " + seriesId + " does not exist");
    } else {
      String dcXML = (String) result.get(SolrFields.XML_KEY);
      DublinCoreCatalog dc;
      try {
        dc = parseDublinCore(dcXML);
      } catch (IOException e) {
        logger.error("Could not parse Dublin core:", e);
        throw new SeriesServiceDatabaseException(e);
      }
      return dc;
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.opencastproject.series.impl.SeriesServiceIndex#getAccessControl(java.lang.String)
   */
  @Override
  public AccessControlList getAccessControl(String seriesID) throws NotFoundException, SeriesServiceDatabaseException {
    SolrDocument seriesDoc = getSolrDocumentByID(seriesID);
    if (seriesDoc == null) {
      logger.debug("No series exists with ID '{}'", seriesID);
      throw new NotFoundException("No series with ID " + seriesID + " found.");
    }
    String serializedAC = (String) seriesDoc.get(SolrFields.ACCESS_CONTROL_KEY);
    AccessControlList accessControl;
    if (serializedAC == null) {
      accessControl = new AccessControlList();
    } else {
      try {
        accessControl = AccessControlParser.parseAcl(serializedAC);
      } catch (Exception e) {
        logger.error("Could not parse access control: {}", e.getMessage());
        throw new SeriesServiceDatabaseException(e);
      }
    }
    return accessControl;
  }

  /**
   * Returns SolrDocument corresponding to given ID or null if such document does not exist.
   *
   * @param id
   *          SolrDocument ID
   * @return corresponding document
   * @throws SeriesServiceDatabaseException
   *           if exception occurred
   */
  protected SolrDocument getSolrDocumentByID(String id) throws SeriesServiceDatabaseException {
    String orgId = securityService.getOrganization().getId();
    StringBuilder solrQueryString = new StringBuilder(SolrFields.COMPOSITE_ID_KEY).append(":")
            .append(ClientUtils.escapeQueryChars(getCompositeKey(id, orgId)));

    SolrQuery q = new SolrQuery(solrQueryString.toString());
    QueryResponse response;
    try {
      response = solrServer.query(q);
      if (response.getResults().isEmpty()) {
        return null;
      } else {
        return response.getResults().get(0);
      }
    } catch (SolrServerException e) {
      logger.error("Could not perform series retrieval:", e);
      throw new SeriesServiceDatabaseException(e);
    }
  }

  /**
   * Clears the index of all series instances.
   */
  public void clear() throws SeriesException {
    try {
      synchronized (solrServer) {
        solrServer.deleteByQuery("*:*");
        solrServer.commit();
      }
    } catch (Exception e) {
      throw new SeriesException(e);
    }
  }

  /**
   * Serializes Dublin core and returns serialized string.
   *
   * @param dc
   *          {@link DublinCoreCatalog} to be serialized
   * @return String representation of serialized Dublin core
   * @throws IOException
   *           if serialization fails
   */
  private String serializeDublinCore(DublinCoreCatalog dc) throws IOException {
    InputStream in = dcService.serialize(dc);

    StringWriter writer = new StringWriter();
    IOUtils.copy(in, writer, "UTF-8");

    return writer.toString();
  }

  /**
   * Parses Dublin core stored as string.
   *
   * @param dcXML
   *          string representation of Dublin core
   * @return parsed {@link DublinCoreCatalog}
   * @throws IOException
   *           if parsing fails
   */
  private DublinCoreCatalog parseDublinCore(String dcXML) throws IOException {
    InputStream in = null;
    try {
      in = IOUtils.toInputStream(dcXML, "UTF-8");
      return dcService.load(in);
    } finally {
      IOUtils.closeQuietly(in);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy