View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.search.impl;
23  
24  import static org.opencastproject.security.api.Permissions.Action.WRITE;
25  import static org.opencastproject.security.util.SecurityUtil.getEpisodeRoleId;
26  
27  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
28  import org.opencastproject.elasticsearch.index.rebuild.AbstractIndexProducer;
29  import org.opencastproject.elasticsearch.index.rebuild.IndexProducer;
30  import org.opencastproject.elasticsearch.index.rebuild.IndexRebuildException;
31  import org.opencastproject.elasticsearch.index.rebuild.IndexRebuildService;
32  import org.opencastproject.list.api.ListProviderException;
33  import org.opencastproject.list.api.ListProvidersService;
34  import org.opencastproject.list.api.ResourceListQuery;
35  import org.opencastproject.list.impl.ResourceListQueryImpl;
36  import org.opencastproject.mediapackage.MediaPackage;
37  import org.opencastproject.metadata.dublincore.DublinCore;
38  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
39  import org.opencastproject.metadata.dublincore.DublinCoreUtil;
40  import org.opencastproject.metadata.dublincore.DublinCoreValue;
41  import org.opencastproject.metadata.dublincore.DublinCores;
42  import org.opencastproject.search.api.SearchException;
43  import org.opencastproject.search.api.SearchResult;
44  import org.opencastproject.search.api.SearchService;
45  import org.opencastproject.search.impl.persistence.SearchServiceDatabase;
46  import org.opencastproject.search.impl.persistence.SearchServiceDatabaseException;
47  import org.opencastproject.security.api.AccessControlEntry;
48  import org.opencastproject.security.api.AccessControlList;
49  import org.opencastproject.security.api.AccessControlUtil;
50  import org.opencastproject.security.api.AuthorizationService;
51  import org.opencastproject.security.api.Organization;
52  import org.opencastproject.security.api.OrganizationDirectoryService;
53  import org.opencastproject.security.api.SecurityService;
54  import org.opencastproject.security.api.UnauthorizedException;
55  import org.opencastproject.security.api.User;
56  import org.opencastproject.security.util.SecurityUtil;
57  import org.opencastproject.series.api.SeriesException;
58  import org.opencastproject.series.api.SeriesService;
59  import org.opencastproject.util.NotFoundException;
60  import org.opencastproject.util.data.Tuple;
61  import org.opencastproject.workspace.api.Workspace;
62  
63  import com.google.gson.Gson;
64  import com.google.gson.JsonElement;
65  
66  import org.apache.commons.io.IOUtils;
67  import org.apache.commons.lang3.BooleanUtils;
68  import org.elasticsearch.ElasticsearchStatusException;
69  import org.elasticsearch.action.DocWriteResponse;
70  import org.elasticsearch.action.index.IndexRequest;
71  import org.elasticsearch.action.search.SearchRequest;
72  import org.elasticsearch.action.search.SearchResponse;
73  import org.elasticsearch.action.update.UpdateRequest;
74  import org.elasticsearch.action.update.UpdateResponse;
75  import org.elasticsearch.client.RequestOptions;
76  import org.elasticsearch.client.indices.CreateIndexRequest;
77  import org.elasticsearch.common.xcontent.XContentType;
78  import org.elasticsearch.rest.RestStatus;
79  import org.elasticsearch.search.builder.SearchSourceBuilder;
80  import org.osgi.service.component.ComponentContext;
81  import org.osgi.service.component.annotations.Activate;
82  import org.osgi.service.component.annotations.Component;
83  import org.osgi.service.component.annotations.Reference;
84  import org.slf4j.Logger;
85  import org.slf4j.LoggerFactory;
86  
87  import java.io.IOException;
88  import java.io.InputStream;
89  import java.nio.charset.StandardCharsets;
90  import java.time.Instant;
91  import java.time.format.DateTimeFormatter;
92  import java.util.ArrayList;
93  import java.util.Collections;
94  import java.util.Date;
95  import java.util.HashMap;
96  import java.util.HashSet;
97  import java.util.List;
98  import java.util.Map;
99  import java.util.Objects;
100 import java.util.Set;
101 import java.util.concurrent.atomic.AtomicInteger;
102 import java.util.stream.Collectors;
103 
104 /**
105  * A Elasticsearch-based {@link SearchService} implementation.
106  */
107 @Component(
108         immediate = true,
109         service = { SearchServiceIndex.class, IndexProducer.class },
110         property = {
111                 "service.description=Search Service Index",
112                 "service.pid=org.opencastproject.search.impl.SearchServiceIndex"
113         }
114 )
115 public final class SearchServiceIndex extends AbstractIndexProducer implements IndexProducer {
116 
117   @Override
118   public IndexRebuildService.Service getService() {
119     return IndexRebuildService.Service.Search;
120   }
121 
122   /** Log facility */
123   private static final Logger logger = LoggerFactory.getLogger(SearchServiceIndex.class);
124 
125   public static final String INDEX_NAME = "opencast_search";
126 
127   private final Gson gson = new Gson();
128 
129   private ElasticsearchIndex esIndex;
130 
131   private SeriesService seriesService;
132 
133   /** The local workspace */
134   private Workspace workspace;
135 
136   /** The security service */
137   private SecurityService securityService;
138 
139   /** The authorization service */
140   private AuthorizationService authorizationService;
141 
142   /** Persistent storage */
143   private SearchServiceDatabase persistence;
144 
145   /** The organization directory service */
146   private OrganizationDirectoryService organizationDirectory = null;
147 
148   private ListProvidersService listProvidersService;
149 
150   private static final String CONFIG_EPISODE_ID_ROLE = "org.opencastproject.episode.id.role.access";
151 
152   private boolean episodeIdRole = false;
153 
154   private String systemUserName = null;
155 
156 
157   /**
158    * Creates a new instance of the search service index.
159    */
160   public SearchServiceIndex() {
161   }
162 
163   /**
164    * Service activator, called via declarative services configuration.
165    *
166    * @param cc
167    *          the component context
168    */
169   @Activate
170   public void activate(final ComponentContext cc) throws IllegalStateException {
171     episodeIdRole = BooleanUtils.toBoolean(Objects.toString(
172         cc.getBundleContext().getProperty(CONFIG_EPISODE_ID_ROLE), "false"));
173     logger.debug("Usage of episode ID roles is set to {}", episodeIdRole);
174 
175     createIndex();
176     systemUserName = SecurityUtil.getSystemUserName(cc);
177   }
178 
179   private void createIndex() {
180     var mapping = "";
181     try (var in = this.getClass().getResourceAsStream("/search-mapping.json")) {
182       mapping = IOUtils.toString(in, StandardCharsets.UTF_8);
183     } catch (IOException e) {
184       throw new SearchException("Could not read mapping.", e);
185     }
186     try {
187       logger.debug("Trying to create index for '{}'", INDEX_NAME);
188       InputStream is = getClass().getResourceAsStream("/elasticsearch/indexSettings.json");
189       String indexSettings = IOUtils.toString(is, StandardCharsets.UTF_8);
190       final CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME)
191           .settings(indexSettings, XContentType.JSON)
192           .mapping(mapping, XContentType.JSON);
193       var response = esIndex.getClient().indices().create(request, RequestOptions.DEFAULT);
194       if (!response.isAcknowledged()) {
195         throw new SearchException("Unable to create index for '" + INDEX_NAME + "'");
196       }
197     } catch (ElasticsearchStatusException e) {
198       if (e.getDetailedMessage().contains("already_exists_exception")) {
199         logger.info("Detected existing index '{}'", INDEX_NAME);
200       } else {
201         throw e;
202       }
203     } catch (IOException e) {
204       throw new SearchException(e);
205     }
206   }
207 
208   @Reference
209   public void setEsIndex(ElasticsearchIndex esIndex) {
210     this.esIndex = esIndex;
211   }
212 
213 
214   public SearchResponse search(SearchSourceBuilder searchSource) throws SearchException {
215     SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
216     logger.debug("Sending for query: {}", searchSource.query());
217     searchRequest.source(searchSource);
218     try {
219       return esIndex.getClient().search(searchRequest, RequestOptions.DEFAULT);
220     } catch (IOException e) {
221       throw new SearchException(e);
222     }
223   }
224 
225   /**
226    * Immediately adds the mediapackage to the search index.
227    *
228    * @param mediaPackage
229    *          the media package
230    * @throws SearchException
231    *           if the media package cannot be added to the search index
232    * @throws IllegalArgumentException
233    *           if the media package is <code>null</code>
234    * @throws UnauthorizedException
235    *           if the user does not have the rights to add the mediapackage
236    */
237   public void addSynchronously(MediaPackage mediaPackage)
238           throws SearchException, IllegalArgumentException, UnauthorizedException, SearchServiceDatabaseException {
239     if (mediaPackage == null) {
240       throw new IllegalArgumentException("Unable to add a null mediapackage");
241     }
242     var mediaPackageId = mediaPackage.getIdentifier().toString();
243 
244     checkSearchEntityWritePermission(mediaPackageId);
245 
246     logger.debug("Attempting to add media package {} to search index", mediaPackageId);
247     final var acls = new AccessControlList[1];
248     final var org = securityService.getOrganization();
249     final var systemUser = SecurityUtil.createSystemUser(systemUserName, org);
250     // Ensure we always get the actual acl by forcing access
251     SecurityUtil.runAs(securityService, org, systemUser, () -> {
252       acls[0] = authorizationService.getActiveAcl(mediaPackage).getA();
253     });
254     var acl = acls[0] == null ? new AccessControlList() : acls[0];
255     var now = new Date();
256 
257     try {
258       persistence.storeMediaPackage(mediaPackage, acl, now);
259     } catch (SearchServiceDatabaseException e) {
260       throw new SearchException(String.format("Could not store media package to search database %s", mediaPackageId),
261           e);
262     }
263 
264     indexMediaPackage(mediaPackage, acl);
265   }
266 
267   public void indexMediaPackage(String mediaPackageId)
268           throws SearchException, SearchServiceDatabaseException, UnauthorizedException, NotFoundException {
269     if (!securityService.getUser().hasRole("ROLE_ADMIN")) {
270       throw new UnauthorizedException("Only global administrators may trigger manual event updates.");
271     }
272     try {
273       MediaPackage mp = persistence.getMediaPackage(mediaPackageId);
274       AccessControlList acl = persistence.getAccessControlList(mediaPackageId);
275       Date modificationDate = persistence.getModificationDate(mediaPackageId);
276       Date deletionDate = persistence.getDeletionDate(mediaPackageId);
277       indexMediaPackage(mp, acl, modificationDate, deletionDate);
278     } catch (RuntimeException e) {
279       logSkippingElement(logger, "event", mediaPackageId, e);
280     }
281   }
282 
283   private void indexMediaPackage(MediaPackage mediaPackage, AccessControlList acl)
284           throws SearchException, SearchServiceDatabaseException {
285     indexMediaPackage(mediaPackage, acl, null, null);
286   }
287 
288   private void indexMediaPackage(MediaPackage mediaPackage, AccessControlList acl, Date modDate, Date delDate)
289           throws SearchException, SearchServiceDatabaseException {
290     String mediaPackageId = mediaPackage.getIdentifier().toString();
291     String orgId = securityService.getOrganization().getId();
292     //If the entry has been deleted then there's *probably* no dc file to load.
293     DublinCoreCatalog dc = null == delDate
294         ? DublinCoreUtil.loadEpisodeDublinCore(workspace, mediaPackage).orElse(DublinCores.mkSimple())
295         : DublinCores.mkSimple();
296 
297     List<DublinCoreCatalog> seriesList = Collections.emptyList();
298     if (dc.hasValue(DublinCore.PROPERTY_IS_PART_OF)) {
299       //Find the series (if any), filter for those which exist to prevent linking non-existent series
300       seriesList = dc.get(DublinCore.PROPERTY_IS_PART_OF).stream().map(DublinCoreValue::getValue).map(s -> {
301         try {
302           return seriesService.getSeries(s);
303         } catch (NotFoundException e) {
304           logger.warn("Series {} not found during index of event {}, omitting the link from the indexed data", s,
305               mediaPackageId);
306         } catch (UnauthorizedException e) {
307           logger.warn("Not authorized for series {} during index of event {}, omitting the link from the indexed data",
308               s, mediaPackageId);
309         } catch (SeriesException e) {
310           throw new SearchException(e);
311         }
312         return null;
313       }).filter(Objects::nonNull).collect(Collectors.toList());
314     }
315 
316     // Add custom roles if enabled
317     acl = addCustomAclRoles(mediaPackageId, acl);
318 
319     SearchResult item = new SearchResult(SearchService.IndexEntryType.Episode, dc, acl, orgId, mediaPackage,
320         null != modDate ? modDate.toInstant() : Instant.now(),
321         null != delDate ? delDate.toInstant() : null);
322     Map<String, Object> metadata = item.dehydrateForIndex();
323     try {
324       var request = new IndexRequest(INDEX_NAME);
325       request.id(mediaPackageId);
326       request.source(metadata);
327       esIndex.getClient().index(request, RequestOptions.DEFAULT);
328       logger.debug("Indexed episode {}", mediaPackageId);
329     } catch (IOException e) {
330       throw new SearchException(e);
331     }
332 
333     // Elasticsearch series
334     for (DublinCoreCatalog seriesDc : seriesList) {
335       String seriesId = seriesDc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
336       AccessControlList seriesAcl = persistence.getAccessControlLists(seriesId, mediaPackageId).stream()
337           .map(aclPair -> addCustomAclRoles(aclPair.getKey(), aclPair.getValue()))
338           .reduce(new AccessControlList(acl.getEntries()), AccessControlList::mergeActions);
339       item = new SearchResult(SearchService.IndexEntryType.Series, seriesDc, seriesAcl, orgId,
340           null, Instant.now(), null);
341 
342       Map<String, Object> seriesData = item.dehydrateForIndex();
343       try {
344         var request = new IndexRequest(INDEX_NAME);
345         request.id(seriesId);
346         request.source(seriesData);
347         esIndex.getClient().index(request, RequestOptions.DEFAULT);
348         logger.debug("Indexed series {} related to episode {}", seriesId, mediaPackageId);
349       } catch (IOException e) {
350         throw new SearchException(e);
351       }
352     }
353   }
354 
355   /**
356    * Add custom roles of the media package to the passed ACL
357    *
358    * @param mediaPackageId
359    *          the media package
360    * @param acl
361    *          the existing access control list
362    * @return {@link AccessControlList} containing the passed and the custom roles merged together
363    *
364    */
365   private AccessControlList addCustomAclRoles(String mediaPackageId, AccessControlList acl) {
366     // This allows users with a role of the form ROLE_EPISODE_<ID>_<ACTION> to access the event through the index
367     if (episodeIdRole) {
368       Set<AccessControlEntry> customEntries = new HashSet<>();
369       customEntries.add(new AccessControlEntry(getEpisodeRoleId(mediaPackageId, "READ"), "read", true));
370       customEntries.add(new AccessControlEntry(getEpisodeRoleId(mediaPackageId, "WRITE"), "write", true));
371 
372       ResourceListQuery query = new ResourceListQueryImpl();
373       if (listProvidersService.hasProvider("ACL.ACTIONS")) {
374         Map<String, String> actions = new HashMap<>();
375         try {
376           actions = listProvidersService.getList("ACL.ACTIONS", query, true);
377         } catch (ListProviderException e) {
378           throw new SearchException("Listproviders not loaded. " + e);
379         }
380         for (String action : actions.keySet()) {
381           customEntries.add(
382               new AccessControlEntry(getEpisodeRoleId(mediaPackageId, action), action, true));
383         }
384       }
385 
386       AccessControlList customRoles = new AccessControlList(new ArrayList<>(customEntries));
387       acl = customRoles.merge(acl);
388     }
389 
390     return acl;
391   }
392 
393   private void checkSearchEntityWritePermission(final String mediaPackageId) throws SearchException {
394     User user = securityService.getUser();
395     try {
396       AccessControlList acl = persistence.getAccessControlList(mediaPackageId);
397       if (!AccessControlUtil.isAuthorized(acl, user, securityService.getOrganization(), WRITE.toString(),
398           mediaPackageId)) {
399         throw new UnauthorizedException(user, "Write permission denied for " + mediaPackageId, acl);
400       }
401     } catch (NotFoundException e) {
402       logger.debug("Mediapackage {} does not exist or was deleted, allowing writes for user {}", mediaPackageId, user);
403     } catch (SearchServiceDatabaseException | UnauthorizedException e) {
404       throw new SearchException(e);
405     }
406   }
407 
408   /**
409    * Immediately removes the given mediapackage from the search service.
410    *
411    * @param mediaPackageId
412    *          the media package identifier
413    * @return <code>true</code> if the mediapackage was deleted
414    * @throws SearchException
415    *           if deletion failed
416    */
417   public boolean deleteSynchronously(final String mediaPackageId) throws SearchException {
418 
419     checkSearchEntityWritePermission(mediaPackageId);
420 
421     String deletionString = DateTimeFormatter.ISO_INSTANT.format(Instant.now());
422 
423     try {
424       logger.info("Marking media package {} as deleted in search index", mediaPackageId);
425       JsonElement json = gson.toJsonTree(Map.of(
426           SearchResult.DELETED_DATE, deletionString,
427           SearchResult.MODIFIED_DATE, deletionString));
428       var updateRequst = new UpdateRequest(INDEX_NAME, mediaPackageId)
429           .doc(gson.toJson(json), XContentType.JSON);
430       esIndex.getClient().update(updateRequst, RequestOptions.DEFAULT);
431     } catch (ElasticsearchStatusException e) {
432       if (e.status().getStatus() != RestStatus.NOT_FOUND.getStatus()) {
433         throw e;
434       }
435       logger.warn("Event {} is not in the search index. Skipping deletion", mediaPackageId);
436     } catch (IOException e) {
437       throw new SearchException("Could not delete episode " + mediaPackageId + " from index", e);
438     }
439 
440     try {
441       logger.info("Marking media package {} as deleted in search database", mediaPackageId);
442 
443       String seriesId = null;
444       Date now = new Date();
445       try {
446         seriesId = persistence.getMediaPackage(mediaPackageId).getSeries();
447         persistence.deleteMediaPackage(mediaPackageId, now);
448         logger.info("Removed media package {} from search persistence", mediaPackageId);
449       } catch (NotFoundException e) {
450         // even if mp not found in persistence, it might still exist in search index.
451         logger.info("Could not find media package with id {} in persistence, but will try remove it from index anyway.",
452             mediaPackageId);
453       } catch (SearchServiceDatabaseException | UnauthorizedException e) {
454         throw new SearchException(
455             String.format("Could not delete media package with id %s from persistence storage", mediaPackageId), e);
456       }
457 
458       // Update series
459       if (seriesId != null) {
460         try {
461           if (!persistence.getSeries(seriesId).isEmpty()) {
462             // Update series acl if there are still episodes in the series
463             final AccessControlList seriesAcl = persistence.getAccessControlLists(seriesId).stream()
464                 .map(aclPair -> addCustomAclRoles(aclPair.getKey(), aclPair.getValue()))
465                 .reduce(new AccessControlList(), AccessControlList::mergeActions);
466             JsonElement json = gson.toJsonTree(Map.of(
467                 SearchResult.INDEX_ACL, SearchResult.dehydrateAclForIndex(seriesAcl),
468                 SearchResult.MODIFIED_DATE, deletionString));
469             var updateRequest = new UpdateRequest(INDEX_NAME, seriesId).doc(gson.toJson(json), XContentType.JSON);
470             try {
471               esIndex.getClient().update(updateRequest, RequestOptions.DEFAULT);
472             } catch (ElasticsearchStatusException e) {
473               if (RestStatus.NOT_FOUND == e.status()) {
474                 logger.warn("Attempted to modify {}, but that series does not exist in the index.", seriesId);
475               }
476             }
477           } else {
478             // Remove series if there are no episodes in the series any longer
479             deleteSeriesSynchronously(seriesId);
480           }
481         } catch (IOException e) {
482           throw new SearchException(e);
483         }
484       }
485 
486       return true;
487     } catch (SearchServiceDatabaseException e) {
488       logger.info("Could not delete media package with id {} from search index", mediaPackageId);
489       throw new SearchException(e);
490     }
491   }
492 
493   /**
494    * Immediately removes the given series from the search service.
495    *
496    * @param seriesId
497    *          the series
498    * @throws SearchException
499    */
500   public boolean deleteSeriesSynchronously(String seriesId) throws SearchException {
501     try {
502       logger.info("Marking {} as deleted in the search index", seriesId);
503       JsonElement json = gson.toJsonTree(Map.of(
504           "deleted", Instant.now().getEpochSecond(),
505           "modified", Instant.now().toString()));
506       var updateRequest = new UpdateRequest(INDEX_NAME, seriesId).doc(gson.toJson(json), XContentType.JSON);
507       try {
508         UpdateResponse response = esIndex.getClient().update(updateRequest, RequestOptions.DEFAULT);
509         //NB: We're marking things as deleted but *not actually deleting them**
510         return DocWriteResponse.Result.UPDATED == response.getResult();
511       } catch (ElasticsearchStatusException e) {
512         if (RestStatus.NOT_FOUND == e.status()) {
513           logger.debug("Attempted to delete {}, but that series does not exist in the index.", seriesId);
514           return true;
515         }
516         throw new SearchException(e);
517       }
518     } catch (IOException e) {
519       throw new SearchException("Could not delete series " + seriesId + " from index", e);
520     }
521   }
522 
523   @Override
524   public void repopulate(IndexRebuildService.DataType type) throws IndexRebuildException {
525     final Organization originalOrg = securityService.getOrganization();
526     final User originalUser = securityService.getUser();
527 
528     try {
529       int total = persistence.countMediaPackages();
530       int pageSize = 50;
531       int pageOffset = 0;
532       AtomicInteger current = new AtomicInteger(1);
533       logIndexRebuildBegin(logger, total, "search");
534       List<Tuple<MediaPackage, String>> page = null;
535 
536       do {
537         page = persistence.getAllMediaPackages(pageSize, pageOffset).collect(Collectors.toList());
538         page.forEach(tuple -> {
539           try {
540             MediaPackage mediaPackage = tuple.getA();
541             Organization organization = organizationDirectory.getOrganization(tuple.getB());
542             final var systemUser = SecurityUtil.createSystemUser(systemUserName, organization);
543             securityService.setUser(systemUser);
544             securityService.setOrganization(organization);
545 
546             String mediaPackageId = mediaPackage.getIdentifier().toString();
547 
548             AccessControlList acl = persistence.getAccessControlList(mediaPackageId);
549             Date modificationDate = persistence.getModificationDate(mediaPackageId);
550             Date deletionDate = persistence.getDeletionDate(mediaPackageId);
551 
552             current.getAndIncrement();
553 
554             indexMediaPackage(mediaPackage, acl, modificationDate, deletionDate);
555           } catch (SearchServiceDatabaseException e) {
556             logIndexRebuildError(logger, total, current.get(), e);
557             //NB: Runtime exception thrown to escape the functional interfacing
558             throw new RuntimeException("Internal Index Rebuild Failure", e);
559           } catch (RuntimeException | NotFoundException e) {
560             logSkippingElement(logger, "event", tuple.getA().getIdentifier().toString(), e);
561           }
562         });
563         //Current is the *page* index, so we remove one since each page only has pageSize entries
564         logIndexRebuildProgress(logger, total, current.get() - 1, pageSize);
565         pageOffset += 1;
566       } while (pageOffset * pageSize <= total);
567       //NB: Catching RuntimeException since it can be thrown inside the functional forEach here
568     } catch (SearchServiceDatabaseException | RuntimeException e) {
569       logIndexRebuildError(logger, e);
570       throw new IndexRebuildException("Index Rebuild Failure", e);
571     } finally {
572       securityService.setUser(originalUser);
573       securityService.setOrganization(originalOrg);
574     }
575   }
576 
577   @Reference
578   public void setPersistence(SearchServiceDatabase persistence) {
579     this.persistence = persistence;
580   }
581 
582   @Reference
583   public void setSeriesService(SeriesService seriesService) {
584     this.seriesService = seriesService;
585   }
586 
587   @Reference
588   public void setWorkspace(Workspace workspace) {
589     this.workspace = workspace;
590   }
591 
592   @Reference
593   public void setAuthorizationService(AuthorizationService authorizationService) {
594     this.authorizationService = authorizationService;
595   }
596 
597   /**
598    * Callback for setting the security service.
599    *
600    * @param securityService
601    *          the securityService to set
602    */
603   @Reference
604   public void setSecurityService(SecurityService securityService) {
605     this.securityService = securityService;
606   }
607 
608   /**
609    * Sets a reference to the organization directory service.
610    *
611    * @param organizationDirectory
612    *          the organization directory
613    */
614   @Reference
615   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
616     this.organizationDirectory = organizationDirectory;
617   }
618 
619   @Reference
620   public void setListProvidersService(ListProvidersService listProvidersService) {
621     this.listProvidersService = listProvidersService;
622   }
623 }