/** * 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.index.service.impl.index.series; import static org.opencastproject.index.service.util.ListProviderUtil.splitStringList; import org.opencastproject.index.service.impl.index.AbstractSearchIndex; import org.opencastproject.index.service.impl.index.event.Event; import org.opencastproject.index.service.impl.index.event.EventSearchQuery; import org.opencastproject.matterhorn.search.SearchIndexException; import org.opencastproject.matterhorn.search.SearchMetadata; import org.opencastproject.matterhorn.search.SearchResult; import org.opencastproject.matterhorn.search.SearchResultItem; import org.opencastproject.matterhorn.search.impl.SearchMetadataCollection; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.EncodingSchemeUtils; import org.opencastproject.security.api.AccessControlEntry; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AccessControlParser; import org.opencastproject.security.api.Permissions; import org.opencastproject.security.api.Permissions.Action; import org.opencastproject.security.api.User; import org.opencastproject.util.DateTimeSupport; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Utility implementation to deal with the conversion of series and its corresponding index data structures. */ public final class SeriesIndexUtils { private static final Logger logger = LoggerFactory.getLogger(SeriesIndexUtils.class); /** * This is a utility class and should therefore not be instantiated. */ private SeriesIndexUtils() { } /** * Creates a search result item based on the data returned from the search index. * * @param metadata * the search metadata * @return the search result item * @throws IOException * if unmarshalling fails */ public static Series toSeries(SearchMetadataCollection metadata) throws IOException { Map<String, SearchMetadata<?>> metadataMap = metadata.toMap(); String seriesXml = (String) metadataMap.get(SeriesIndexSchema.OBJECT).getValue(); return Series.valueOf(IOUtils.toInputStream(seriesXml)); } /** * Creates search metadata from a series such that the event can be stored in the search index. * * @param series * the series * @return the set of metadata */ public static SearchMetadataCollection toSearchMetadata(Series series) { SearchMetadataCollection metadata = new SearchMetadataCollection( series.getIdentifier().concat(series.getOrganization()), Series.DOCUMENT_TYPE); metadata.addField(SeriesIndexSchema.UID, series.getIdentifier(), true); metadata.addField(SeriesIndexSchema.ORGANIZATION, series.getOrganization(), false); metadata.addField(SeriesIndexSchema.OBJECT, series.toXML(), false); metadata.addField(SeriesIndexSchema.TITLE, series.getTitle(), true); if (StringUtils.trimToNull(series.getDescription()) != null) { metadata.addField(SeriesIndexSchema.DESCRIPTION, series.getDescription(), true); } if (StringUtils.trimToNull(series.getSubject()) != null) { metadata.addField(SeriesIndexSchema.SUBJECT, series.getSubject(), true); } if (StringUtils.trimToNull(series.getLanguage()) != null) { metadata.addField(SeriesIndexSchema.LANGUAGE, series.getLanguage(), true); } if (StringUtils.trimToNull(series.getCreator()) != null) { metadata.addField(SeriesIndexSchema.CREATOR, series.getCreator(), true); } if (StringUtils.trimToNull(series.getLicense()) != null) { metadata.addField(SeriesIndexSchema.LICENSE, series.getLicense(), true); } if (StringUtils.trimToNull(series.getManagedAcl()) != null) { metadata.addField(SeriesIndexSchema.MANAGED_ACL, series.getManagedAcl(), true); } if (series.getCreatedDateTime() != null) { metadata.addField(SeriesIndexSchema.CREATED_DATE_TIME, DateTimeSupport.toUTC(series.getCreatedDateTime().getTime()), true); } if (series.getOrganizers() != null) { metadata.addField(SeriesIndexSchema.ORGANIZERS, series.getOrganizers().toArray(), true); } if (series.getContributors() != null) { metadata.addField(SeriesIndexSchema.CONTRIBUTORS, series.getContributors().toArray(), true); } if (series.getPublishers() != null) { metadata.addField(SeriesIndexSchema.PUBLISHERS, series.getPublishers().toArray(), true); } if (series.getRightsHolder() != null) { metadata.addField(SeriesIndexSchema.RIGHTS_HOLDER, series.getRightsHolder(), true); } if (StringUtils.trimToNull(series.getAccessPolicy()) != null) { metadata.addField(SeriesIndexSchema.ACCESS_POLICY, series.getAccessPolicy(), true); addAuthorization(metadata, series.getAccessPolicy()); } metadata.addField(SeriesIndexSchema.OPT_OUT, series.isOptedOut(), false); if (series.getTheme() != null) { metadata.addField(SeriesIndexSchema.THEME, series.getTheme(), false); } return metadata; } /** * Adds authorization fields to the input document. * * @param doc * the input document * @param aclString * the access control list string */ private static void addAuthorization(SearchMetadataCollection doc, String aclString) { Map<String, List<String>> permissions = new HashMap<String, List<String>>(); // Define containers for common permissions for (Action action : Permissions.Action.values()) { permissions.put(action.toString(), new ArrayList<String>()); } AccessControlList acl = AccessControlParser.parseAclSilent(aclString); for (AccessControlEntry entry : acl.getEntries()) { if (!entry.isAllow()) { logger.info("Series index does not support denial via ACL, ignoring {}", entry); continue; } List<String> actionPermissions = permissions.get(entry.getAction()); if (actionPermissions == null) { actionPermissions = new ArrayList<String>(); permissions.put(entry.getAction(), actionPermissions); } actionPermissions.add(entry.getRole()); } // Write the permissions to the input document for (Map.Entry<String, List<String>> entry : permissions.entrySet()) { String fieldName = SeriesIndexSchema.ACL_PERMISSION_PREFIX.concat(entry.getKey()); doc.addField(fieldName, entry.getValue(), false); } } /** * Loads the series from the search index or creates a new one that can then be persisted. * * @param seriesId * the series identifier * @param organization * the organization * @param user * the user * @param searchIndex * the AdminUISearchIndex to search in * @return the series * @throws SearchIndexException * if querying the search index fails * @throws IllegalStateException * if multiple series with the same identifier are found */ public static Series getOrCreate(String seriesId, String organization, User user, AbstractSearchIndex searchIndex) throws SearchIndexException { SeriesSearchQuery query = new SeriesSearchQuery(organization, user).withoutActions().withIdentifier(seriesId); SearchResult<Series> searchResult = searchIndex.getByQuery(query); if (searchResult.getDocumentCount() == 0) { return new Series(seriesId, organization); } else if (searchResult.getDocumentCount() == 1) { return searchResult.getItems()[0].getSource(); } else { throw new IllegalStateException("Multiple series with identifier " + seriesId + " found in search index"); } } /** * Update the given {@link Series} with the given {@link DublinCore}. * * @param series * the series to update * @param dc * the catalog with the metadata for the update * @return the updated series */ public static Series updateSeries(Series series, DublinCore dc) { series.setTitle(dc.getFirst(DublinCoreCatalog.PROPERTY_TITLE)); series.setDescription(dc.getFirst(DublinCore.PROPERTY_DESCRIPTION)); series.setSubject(dc.getFirst(DublinCore.PROPERTY_SUBJECT)); series.setLanguage(dc.getFirst(DublinCoreCatalog.PROPERTY_LANGUAGE)); series.setLicense(dc.getFirst(DublinCoreCatalog.PROPERTY_LICENSE)); series.setRightsHolder(dc.getFirst(DublinCore.PROPERTY_RIGHTS_HOLDER)); String createdDateStr = dc.getFirst(DublinCoreCatalog.PROPERTY_CREATED); if (createdDateStr != null) { series.setCreatedDateTime(EncodingSchemeUtils.decodeDate(createdDateStr)); } series.setPublishers(splitStringList(dc.get(DublinCore.PROPERTY_PUBLISHER, DublinCore.LANGUAGE_ANY))); series.setContributors(splitStringList(dc.get(DublinCore.PROPERTY_CONTRIBUTOR, DublinCore.LANGUAGE_ANY))); series.setOrganizers(splitStringList(dc.get(DublinCoreCatalog.PROPERTY_CREATOR, DublinCore.LANGUAGE_ANY))); return series; } public static void updateEventSeriesTitles(Series series, String organization, User user, AbstractSearchIndex searchIndex) throws SearchIndexException { if (!series.isSeriesTitleUpdated()) return; SearchResult<Event> events = searchIndex .getByQuery(new EventSearchQuery(organization, user).withoutActions().withSeriesId(series.getIdentifier())); for (SearchResultItem<Event> searchResultItem : events.getItems()) { Event event = searchResultItem.getSource(); event.setSeriesName(series.getTitle()); searchIndex.addOrUpdate(event); } } /** * Update a unique managed acl name to a new one for all of the series. * * @param currentManagedAcl * The current name for the managed acl. * @param newManagedAcl * The new name for the managed acl. * @param organization * The organization for the managed acl. * @param user * The user. * @param searchIndex * The search index to update the managed acl name. */ public static void updateManagedAclName(String currentManagedAcl, String newManagedAcl, String organization, User user, AbstractSearchIndex searchIndex) { SearchResult<Series> result = null; try { result = searchIndex .getByQuery(new SeriesSearchQuery(organization, user).withoutActions().withManagedAcl(currentManagedAcl)); } catch (SearchIndexException e) { logger.error("Unable to find the series in org '{}' with current managed acl name '{}' because {}", new Object[] { organization, currentManagedAcl, ExceptionUtils.getStackTrace(e) }); } if (result != null && result.getHitCount() > 0) { for (SearchResultItem<Series> seriesItem : result.getItems()) { Series series = seriesItem.getSource(); series.setManagedAcl(newManagedAcl); try { searchIndex.addOrUpdate(series); } catch (SearchIndexException e) { logger.warn( "Unable to update event '{}' from current managed acl '{}' to new managed acl name '{}' because {}", new Object[] { series, currentManagedAcl, newManagedAcl, ExceptionUtils.getStackTrace(e) }); } } } } /** * Delete a managed acl from all of the series that reference it. * * @param managedAcl * The managed acl's unique name that will be removed. * @param organization * The organization for the managed acl * @param user * The user * @param searchIndex * The search index to remove the managed acl from. */ public static void deleteManagedAcl(String managedAcl, String organization, User user, AbstractSearchIndex searchIndex) { SearchResult<Series> result = null; try { result = searchIndex .getByQuery(new SeriesSearchQuery(organization, user).withoutActions().withManagedAcl(managedAcl)); } catch (SearchIndexException e) { logger.error("Unable to find the series in org '{}' with current managed acl name '{}' because {}", new Object[] { organization, managedAcl, ExceptionUtils.getStackTrace(e) }); } if (result != null && result.getHitCount() > 0) { for (SearchResultItem<Series> seriesItem : result.getItems()) { Series series = seriesItem.getSource(); series.setManagedAcl(null); try { searchIndex.addOrUpdate(series); } catch (SearchIndexException e) { logger.warn("Unable to update series '{}' to remove managed acl '{}' because {}", new Object[] { series, managedAcl, ExceptionUtils.getStackTrace(e) }); } } } } }