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.themes.persistence;
23  
24  import static org.opencastproject.db.Queries.namedQuery;
25  
26  import org.opencastproject.db.DBSession;
27  import org.opencastproject.db.DBSessionFactory;
28  import org.opencastproject.elasticsearch.api.SearchIndexException;
29  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
30  import org.opencastproject.elasticsearch.index.objects.theme.IndexTheme;
31  import org.opencastproject.elasticsearch.index.rebuild.AbstractIndexProducer;
32  import org.opencastproject.elasticsearch.index.rebuild.IndexProducer;
33  import org.opencastproject.elasticsearch.index.rebuild.IndexRebuildException;
34  import org.opencastproject.elasticsearch.index.rebuild.IndexRebuildService;
35  import org.opencastproject.security.api.Organization;
36  import org.opencastproject.security.api.OrganizationDirectoryService;
37  import org.opencastproject.security.api.SecurityService;
38  import org.opencastproject.security.api.User;
39  import org.opencastproject.security.api.UserDirectoryService;
40  import org.opencastproject.themes.Theme;
41  import org.opencastproject.themes.ThemesServiceDatabase;
42  import org.opencastproject.util.NotFoundException;
43  
44  import org.apache.commons.lang3.StringUtils;
45  import org.apache.commons.lang3.tuple.Pair;
46  import org.osgi.service.component.ComponentContext;
47  import org.osgi.service.component.annotations.Activate;
48  import org.osgi.service.component.annotations.Component;
49  import org.osgi.service.component.annotations.Reference;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  import java.util.ArrayList;
54  import java.util.List;
55  import java.util.Optional;
56  import java.util.function.Function;
57  import java.util.stream.Collectors;
58  
59  import javax.persistence.EntityManager;
60  import javax.persistence.EntityManagerFactory;
61  
62  /**
63   * Implements {@link ThemesServiceDatabase}. Defines permanent storage for themes.
64   */
65  @Component(
66      immediate = true,
67      service = { ThemesServiceDatabase.class, IndexProducer.class },
68      property = {
69          "service.description=Themes Database Service"
70      }
71  )
72  public class ThemesServiceDatabaseImpl extends AbstractIndexProducer implements ThemesServiceDatabase {
73  
74    public static final String PERSISTENCE_UNIT = "org.opencastproject.themes";
75  
76    /** Logging utilities */
77    private static final Logger logger = LoggerFactory.getLogger(ThemesServiceDatabaseImpl.class);
78  
79    /** Factory used to create {@link EntityManager}s for transactions */
80    protected EntityManagerFactory emf;
81  
82    protected DBSessionFactory dbSessionFactory;
83  
84    protected DBSession db;
85  
86    /** The security service */
87    protected SecurityService securityService;
88  
89    /** The user directory service */
90    protected UserDirectoryService userDirectoryService;
91  
92    /** The organization directory service to get organizations */
93    protected OrganizationDirectoryService organizationDirectoryService;
94  
95    /** The elasticsearch indices */
96    protected ElasticsearchIndex index;
97  
98    /** The component context for this themes service database */
99    private ComponentContext cc;
100 
101   /**
102    * Creates {@link EntityManagerFactory} using persistence provider and properties passed via OSGi.
103    *
104    * @param cc
105    */
106   @Activate
107   public void activate(ComponentContext cc) {
108     logger.info("Activating persistence manager for themes");
109     this.cc = cc;
110     db = dbSessionFactory.createSession(emf);
111   }
112 
113   /** OSGi DI */
114   @Reference(target = "(osgi.unit.name=org.opencastproject.themes)")
115   public void setEntityManagerFactory(EntityManagerFactory emf) {
116     this.emf = emf;
117   }
118 
119   @Reference
120   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
121     this.dbSessionFactory = dbSessionFactory;
122   }
123 
124   /**
125    * OSGi callback to set the security service.
126    *
127    * @param securityService
128    *          the security service
129    */
130   @Reference
131   public void setSecurityService(SecurityService securityService) {
132     this.securityService = securityService;
133   }
134 
135   /**
136    * OSGi callback to set the user directory service
137    *
138    * @param userDirectoryService
139    *          the user directory service
140    */
141   @Reference
142   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
143     this.userDirectoryService = userDirectoryService;
144   }
145 
146   /** OSGi DI */
147   @Reference
148   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
149     this.organizationDirectoryService = organizationDirectoryService;
150   }
151 
152   /** OSGi DI */
153   @Reference
154   public void setIndex(ElasticsearchIndex index) {
155     this.index = index;
156   }
157 
158   @Override
159   public Theme getTheme(long id) throws ThemesServiceDatabaseException, NotFoundException {
160     try {
161       return db.exec(getThemeDtoQuery(id))
162           .map(t -> t.toTheme(userDirectoryService))
163           .orElseThrow(() -> new NotFoundException("No theme with id=" + id + " exists"));
164     } catch (NotFoundException e) {
165       throw e;
166     } catch (Exception e) {
167       logger.error("Could not get theme", e);
168       throw new ThemesServiceDatabaseException(e);
169     }
170   }
171 
172   private List<Theme> getThemes() throws ThemesServiceDatabaseException {
173     try {
174       String orgId = securityService.getOrganization().getId();
175       return db.exec(namedQuery.findAll(
176           "Themes.findByOrg",
177               ThemeDto.class,
178               Pair.of("org", orgId)
179           )).stream()
180           .map(t -> t.toTheme(userDirectoryService))
181           .collect(Collectors.toList());
182     } catch (Exception e) {
183       logger.error("Could not get themes", e);
184       throw new ThemesServiceDatabaseException(e);
185     }
186   }
187 
188   @Override
189   public Theme updateTheme(final Theme theme) throws ThemesServiceDatabaseException {
190     try {
191       Theme newTheme = db.execTxChecked(em -> {
192         ThemeDto themeDto = null;
193         if (theme.getId().isSome()) {
194           themeDto = getThemeDtoQuery(theme.getId().get()).apply(em).orElse(null);
195         }
196 
197         if (themeDto == null) {
198           // no theme stored, create new entity
199           themeDto = new ThemeDto();
200           themeDto.setOrganization(securityService.getOrganization().getId());
201           updateTheme(theme, themeDto);
202           em.persist(themeDto);
203         } else {
204           updateTheme(theme, themeDto);
205           em.merge(themeDto);
206         }
207 
208         return themeDto.toTheme(userDirectoryService);
209       });
210 
211       // update the elasticsearch indices
212       String orgId = securityService.getOrganization().getId();
213       User user = securityService.getUser();
214       updateThemeInIndex(newTheme, orgId, user);
215 
216       return newTheme;
217     } catch (Exception e) {
218       logger.error("Could not update theme {}", theme, e);
219       throw new ThemesServiceDatabaseException(e);
220     }
221   }
222 
223   private void updateTheme(Theme theme, ThemeDto themeDto) {
224     if (theme.getId().isSome()) {
225       themeDto.setId(theme.getId().get());
226     }
227     themeDto.setUsername(theme.getCreator().getUsername());
228     themeDto.setCreationDate(theme.getCreationDate());
229     themeDto.setDefault(theme.isDefault());
230     themeDto.setName(theme.getName());
231     themeDto.setDescription(theme.getDescription());
232     themeDto.setBumperActive(theme.isBumperActive());
233     themeDto.setBumperFile(theme.getBumperFile());
234     themeDto.setTrailerActive(theme.isTrailerActive());
235     themeDto.setTrailerFile(theme.getTrailerFile());
236     themeDto.setTitleSlideActive(theme.isTitleSlideActive());
237     themeDto.setTitleSlideBackground(theme.getTitleSlideBackground());
238     themeDto.setTitleSlideMetadata(theme.getTitleSlideMetadata());
239     themeDto.setLicenseSlideActive(theme.isLicenseSlideActive());
240     themeDto.setLicenseSlideBackground(theme.getLicenseSlideBackground());
241     themeDto.setLicenseSlideDescription(theme.getLicenseSlideDescription());
242     themeDto.setWatermarkActive(theme.isWatermarkActive());
243     themeDto.setWatermarkFile(theme.getWatermarkFile());
244     themeDto.setWatermarkPosition(theme.getWatermarkPosition());
245   }
246 
247   @Override
248   public void deleteTheme(long id) throws ThemesServiceDatabaseException, NotFoundException {
249     try {
250       db.execTxChecked(em -> {
251         ThemeDto themeDto = getThemeDtoQuery(id).apply(em)
252             .orElseThrow(() -> new NotFoundException("No theme with id=" + id + " exists"));
253         namedQuery.remove(themeDto).accept(em);
254       });
255 
256       // update the elasticsearch indices
257       String organization = securityService.getOrganization().getId();
258       removeThemeFromIndex(id, organization);
259     } catch (NotFoundException e) {
260       throw e;
261     } catch (Exception e) {
262       logger.error("Could not delete theme '{}'", id, e);
263       throw new ThemesServiceDatabaseException(e);
264     }
265   }
266 
267   @Override
268   public int countThemes() throws ThemesServiceDatabaseException {
269     try {
270       String orgId = securityService.getOrganization().getId();
271       return db.exec(namedQuery.find(
272           "Themes.count",
273           Number.class,
274           Pair.of("org", orgId)
275       )).intValue();
276     } catch (Exception e) {
277       logger.error("Could not count themes", e);
278       throw new ThemesServiceDatabaseException(e);
279     }
280   }
281 
282   /**
283    * Gets a theme by its ID, using the current organizational context.
284    *
285    * @param id
286    *          the theme identifier
287    * @return a query function returning an optional theme entity
288    */
289   private Function<EntityManager, Optional<ThemeDto>> getThemeDtoQuery(long id) {
290     String orgId = securityService.getOrganization().getId();
291     return namedQuery.findOpt(
292         "Themes.findById",
293         ThemeDto.class,
294         Pair.of("id", id),
295         Pair.of("org", orgId)
296     );
297   }
298 
299   @Override
300   public void repopulate(IndexRebuildService.DataType type) throws IndexRebuildException {
301     try {
302       for (final Organization organization : organizationDirectoryService.getOrganizations()) {
303         try {
304           final List<Theme> themes = getThemes();
305           int total = themes.size();
306           logIndexRebuildBegin(logger, total, "themes", organization);
307           int current = 0;
308           int n = 20;
309           List<IndexTheme> updatedThemeRange = new ArrayList<>();
310 
311           for (Theme theme : themes) {
312             current++;
313 
314             var updatedThemeData = index.getTheme(theme.getId().get(), organization.toString(),
315                         securityService.getUser());
316             updatedThemeData = getThemeUpdateFunction(theme, organization.toString()).apply(updatedThemeData);
317             updatedThemeRange.add(updatedThemeData.get());
318 
319             if (updatedThemeRange.size() >= n || current >= themes.size()) {
320               index.bulkThemeUpdate(updatedThemeRange);
321               logIndexRebuildProgress(logger, total, current, n);
322               updatedThemeRange.clear();
323             }
324           }
325         } catch (ThemesServiceDatabaseException e) {
326           logger.error("Unable to get themes from the database", e);
327           throw new IllegalStateException(e);
328         }
329       }
330     } catch (Exception e) {
331       logIndexRebuildError(logger, e);
332       throw new IndexRebuildException(getService(), e);
333     }
334   }
335 
336   @Override
337   public IndexRebuildService.Service getService() {
338     return IndexRebuildService.Service.Themes;
339   }
340 
341   /**
342    * Remove the theme from the ElasticSearch index.
343    *
344    * @param themeId
345    *           the id of the theme to remove
346    * @param orgId
347    *           the organization the theme belongs to
348    */
349   private void removeThemeFromIndex(long themeId, String orgId) {
350     logger.debug("Removing theme {} from the {} index.", themeId, index.getIndexName());
351 
352     try {
353       index.deleteTheme(Long.toString(themeId), orgId);
354       logger.debug("Theme {} removed from the {} index", themeId, index.getIndexName());
355     } catch (SearchIndexException e) {
356       logger.error("Error deleting the theme {} from the {} index", themeId, index.getIndexName(), e);
357     }
358   }
359 
360   /**
361    * Update the theme in the ElasticSearch index.
362    *  @param theme
363    *           the theme to update
364    * @param orgId
365    *           the organization the theme belongs to
366    * @param user
367    */
368   private void updateThemeInIndex(Theme theme, String orgId,
369           User user) {
370     logger.debug("Updating the theme with id '{}', name '{}', description '{}', organization '{}' in the {} index.",
371             theme.getId(), theme.getName(), theme.getDescription(),
372             orgId, index.getIndexName());
373     try {
374       if (theme.getId().isNone()) {
375         throw new IllegalArgumentException("Can't put theme in index without valid id!");
376       }
377       Long id = theme.getId().get();
378 
379       // the function to do the actual updating
380       Function<Optional<IndexTheme>, Optional<IndexTheme>> updateFunction = (Optional<IndexTheme> indexThemeOpt) -> {
381         IndexTheme indexTheme;
382         indexTheme = indexThemeOpt.orElseGet(() -> new IndexTheme(id, orgId));
383         String creator = StringUtils.isNotBlank(theme.getCreator().getName())
384                 ? theme.getCreator().getName() : theme.getCreator().getUsername();
385 
386         indexTheme.setCreationDate(theme.getCreationDate());
387         indexTheme.setDefault(theme.isDefault());
388         indexTheme.setName(theme.getName());
389         indexTheme.setDescription(theme.getDescription());
390         indexTheme.setCreator(creator);
391         indexTheme.setBumperActive(theme.isBumperActive());
392         indexTheme.setBumperFile(theme.getBumperFile());
393         indexTheme.setTrailerActive(theme.isTrailerActive());
394         indexTheme.setTrailerFile(theme.getTrailerFile());
395         indexTheme.setTitleSlideActive(theme.isTitleSlideActive());
396         indexTheme.setTitleSlideBackground(theme.getTitleSlideBackground());
397         indexTheme.setTitleSlideMetadata(theme.getTitleSlideMetadata());
398         indexTheme.setLicenseSlideActive(theme.isLicenseSlideActive());
399         indexTheme.setLicenseSlideBackground(theme.getLicenseSlideBackground());
400         indexTheme.setLicenseSlideDescription(theme.getLicenseSlideDescription());
401         indexTheme.setWatermarkActive(theme.isWatermarkActive());
402         indexTheme.setWatermarkFile(theme.getWatermarkFile());
403         indexTheme.setWatermarkPosition(theme.getWatermarkPosition());
404         return Optional.of(indexTheme);
405       };
406 
407       index.addOrUpdateTheme(id, updateFunction, orgId, user);
408       logger.debug("Updated the theme {} in the {} index", theme.getId(), index.getIndexName());
409     } catch (SearchIndexException e) {
410       logger.error("Error updating the theme {} in the {} index", theme.getId(), index.getIndexName(), e);
411     }
412   }
413   /**
414    * Get the function to update a theme in the Elasticsearch index.
415    *
416    * @param theme
417    *          The theme to update
418    * @param orgId
419    *          The id of the current organization
420    * @return the function to do the update
421    */
422   private Function<Optional<IndexTheme>, Optional<IndexTheme>> getThemeUpdateFunction(Theme theme, String orgId) {
423     return (Optional<IndexTheme> indexThemeOpt) -> {
424       IndexTheme indexTheme;
425       indexTheme = indexThemeOpt.orElseGet(() -> new IndexTheme(theme.getId().get(), orgId));
426       String creator = theme.getCreator() == null ? "?" : (
427           StringUtils.isNotBlank(theme.getCreator().getName())
428               ? theme.getCreator().getName()
429               : theme.getCreator().getUsername());
430 
431       indexTheme.setCreationDate(theme.getCreationDate());
432       indexTheme.setDefault(theme.isDefault());
433       indexTheme.setName(theme.getName());
434       indexTheme.setDescription(theme.getDescription());
435       indexTheme.setCreator(creator);
436       indexTheme.setBumperActive(theme.isBumperActive());
437       indexTheme.setBumperFile(theme.getBumperFile());
438       indexTheme.setTrailerActive(theme.isTrailerActive());
439       indexTheme.setTrailerFile(theme.getTrailerFile());
440       indexTheme.setTitleSlideActive(theme.isTitleSlideActive());
441       indexTheme.setTitleSlideBackground(theme.getTitleSlideBackground());
442       indexTheme.setTitleSlideMetadata(theme.getTitleSlideMetadata());
443       indexTheme.setLicenseSlideActive(theme.isLicenseSlideActive());
444       indexTheme.setLicenseSlideBackground(theme.getLicenseSlideBackground());
445       indexTheme.setLicenseSlideDescription(theme.getLicenseSlideDescription());
446       indexTheme.setWatermarkActive(theme.isWatermarkActive());
447       indexTheme.setWatermarkFile(theme.getWatermarkFile());
448       indexTheme.setWatermarkPosition(theme.getWatermarkPosition());
449       return Optional.of(indexTheme);
450     };
451   }
452 }