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.security.api.SecurityService;
29  import org.opencastproject.security.api.UserDirectoryService;
30  import org.opencastproject.themes.Theme;
31  import org.opencastproject.themes.ThemesServiceDatabase;
32  import org.opencastproject.util.NotFoundException;
33  import org.opencastproject.util.requests.SortCriterion;
34  
35  import org.apache.commons.lang3.tuple.Pair;
36  import org.osgi.service.component.ComponentContext;
37  import org.osgi.service.component.annotations.Activate;
38  import org.osgi.service.component.annotations.Component;
39  import org.osgi.service.component.annotations.Reference;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import java.util.ArrayList;
44  import java.util.List;
45  import java.util.Optional;
46  import java.util.function.Function;
47  import java.util.stream.Collectors;
48  
49  import javax.persistence.EntityManager;
50  import javax.persistence.EntityManagerFactory;
51  import javax.persistence.TypedQuery;
52  import javax.persistence.criteria.CriteriaBuilder;
53  import javax.persistence.criteria.CriteriaQuery;
54  import javax.persistence.criteria.Expression;
55  import javax.persistence.criteria.Order;
56  import javax.persistence.criteria.Predicate;
57  import javax.persistence.criteria.Root;
58  
59  /**
60   * Implements {@link ThemesServiceDatabase}. Defines permanent storage for themes.
61   */
62  @Component(
63      immediate = true,
64      service = { ThemesServiceDatabase.class },
65      property = {
66          "service.description=Themes Database Service"
67      }
68  )
69  public class ThemesServiceDatabaseImpl implements ThemesServiceDatabase {
70  
71    public static final String PERSISTENCE_UNIT = "org.opencastproject.themes";
72  
73    /** Logging utilities */
74    private static final Logger logger = LoggerFactory.getLogger(ThemesServiceDatabaseImpl.class);
75  
76    /** Factory used to create {@link EntityManager}s for transactions */
77    protected EntityManagerFactory emf;
78  
79    protected DBSessionFactory dbSessionFactory;
80  
81    protected DBSession db;
82  
83    /** The security service */
84    protected SecurityService securityService;
85  
86    /** The user directory service */
87    protected UserDirectoryService userDirectoryService;
88  
89    /** The component context for this themes service database */
90    private ComponentContext cc;
91  
92    /**
93     * Creates {@link EntityManagerFactory} using persistence provider and properties passed via OSGi.
94     *
95     * @param cc
96     */
97    @Activate
98    public void activate(ComponentContext cc) {
99      logger.info("Activating persistence manager for themes");
100     this.cc = cc;
101     db = dbSessionFactory.createSession(emf);
102   }
103 
104   /** OSGi DI */
105   @Reference(target = "(osgi.unit.name=org.opencastproject.themes)")
106   public void setEntityManagerFactory(EntityManagerFactory emf) {
107     this.emf = emf;
108   }
109 
110   @Reference
111   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
112     this.dbSessionFactory = dbSessionFactory;
113   }
114 
115   /**
116    * OSGi callback to set the security service.
117    *
118    * @param securityService
119    *          the security service
120    */
121   @Reference
122   public void setSecurityService(SecurityService securityService) {
123     this.securityService = securityService;
124   }
125 
126   /**
127    * OSGi callback to set the user directory service
128    *
129    * @param userDirectoryService
130    *          the user directory service
131    */
132   @Reference
133   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
134     this.userDirectoryService = userDirectoryService;
135   }
136 
137   @Override
138   public Theme getTheme(long id) throws ThemesServiceDatabaseException, NotFoundException {
139     try {
140       return db.exec(getThemeDtoQuery(id))
141           .map(t -> t.toTheme(userDirectoryService))
142           .orElseThrow(() -> new NotFoundException("No theme with id=" + id + " exists"));
143     } catch (NotFoundException e) {
144       throw e;
145     } catch (Exception e) {
146       logger.error("Could not get theme", e);
147       throw new ThemesServiceDatabaseException(e);
148     }
149   }
150 
151   private List<Theme> getThemes() throws ThemesServiceDatabaseException {
152     try {
153       String orgId = securityService.getOrganization().getId();
154       return db.exec(namedQuery.findAll(
155           "Themes.findByOrg",
156               ThemeDto.class,
157               Pair.of("org", orgId)
158           )).stream()
159           .map(t -> t.toTheme(userDirectoryService))
160           .collect(Collectors.toList());
161     } catch (Exception e) {
162       logger.error("Could not get themes", e);
163       throw new ThemesServiceDatabaseException(e);
164     }
165   }
166 
167   public List<Theme> findThemes(
168       Optional<Integer> limit,
169       Optional<Integer> offset,
170       ArrayList<SortCriterion> sortCriteria,
171       Optional<String> creatorFilter,
172       Optional<String> textFilter
173   ) {
174     String orgId = securityService.getOrganization().getId();
175 
176     return db.execTxChecked(em -> {
177       CriteriaBuilder cb = em.getCriteriaBuilder();
178       final CriteriaQuery<ThemeDto> query = cb.createQuery(ThemeDto.class);
179       Root<ThemeDto> theme = query.from(ThemeDto.class);
180       query.select(theme);
181       query.distinct(true);
182 
183       // filter
184       List<Predicate> conditions = new ArrayList<>();
185       conditions.add(cb.equal(theme.get("organization"), orgId));
186 
187       // exact match, case sensitive
188       if (creatorFilter.isPresent()) {
189         conditions.add(cb.equal(theme.get("username"), creatorFilter.get()));
190       }
191       // not exact match, case-insensitive, each token needs to match at least one field
192       if (textFilter.isPresent()) {
193         List<Predicate> fulltextConditions = new ArrayList<>();
194         String[] tokens = textFilter.get().split("\\s+");
195         for (String token: tokens) {
196           List<Predicate> fieldConditions = new ArrayList<>();
197           Expression<String> literal = cb.literal("%" + token + "%");
198 
199           fieldConditions.add(cb.like(cb.lower(theme.get("username")), cb.lower(literal)));
200           fieldConditions.add(cb.like(cb.lower(theme.get("name")), cb.lower(literal)));
201           fieldConditions.add(cb.like(cb.lower(theme.get("description")), cb.lower(literal)));
202 
203           // token needs to match at least one field
204           fulltextConditions.add(cb.or(fieldConditions.toArray(new Predicate[0])));
205         }
206         // all token have to match something
207         // (different to fulltext search for Elasticsearch, where only one token has to match!)
208         conditions.add(cb.and(fulltextConditions.toArray(new Predicate[0])));
209       }
210       query.where(cb.and(conditions.toArray(new Predicate[0])));
211 
212       // sort
213       List<Order> orders = new ArrayList<>();
214       for (SortCriterion criterion : sortCriteria) {
215         String fieldName = criterion.getFieldName();
216         switch(fieldName) {
217           case "name":
218             break;
219           case "description":
220             break;
221           case "creator":
222             fieldName = "username";
223             break;
224           case "default":
225             fieldName = "isDefault";
226             break;
227           case "creation_date":
228             fieldName = "creationDate";
229             break;
230           default:
231             throw new IllegalArgumentException("Sorting criterion " + criterion.getFieldName() + " is not supported "
232                 + "for themes.");
233         }
234 
235         Expression expression = theme.get(fieldName);
236         if (criterion.getOrder() == SortCriterion.Order.Ascending) {
237           orders.add(cb.asc(expression));
238         } else if (criterion.getOrder() == SortCriterion.Order.Descending) {
239           orders.add(cb.desc(expression));
240         }
241 
242       }
243       query.orderBy(orders);
244 
245       // other
246       TypedQuery<ThemeDto> typedQuery = em.createQuery(query);
247       if (limit.isPresent()) {
248         typedQuery.setMaxResults(limit.get());
249       }
250       if (offset.isPresent()) {
251         typedQuery.setFirstResult(offset.get());
252       }
253 
254       return typedQuery.getResultList().stream()
255           .map(t -> t.toTheme(userDirectoryService))
256           .collect(Collectors.toList());
257     });
258   }
259 
260   @Override
261   public Theme updateTheme(final Theme theme) throws ThemesServiceDatabaseException {
262     try {
263       Theme newTheme = db.execTxChecked(em -> {
264         ThemeDto themeDto = null;
265         if (theme.getId().isPresent()) {
266           themeDto = getThemeDtoQuery(theme.getId().get()).apply(em).orElse(null);
267         }
268 
269         if (themeDto == null) {
270           // no theme stored, create new entity
271           themeDto = new ThemeDto();
272           themeDto.setOrganization(securityService.getOrganization().getId());
273           updateTheme(theme, themeDto);
274           em.persist(themeDto);
275         } else {
276           updateTheme(theme, themeDto);
277           em.merge(themeDto);
278         }
279 
280         return themeDto.toTheme(userDirectoryService);
281       });
282 
283       return newTheme;
284     } catch (Exception e) {
285       logger.error("Could not update theme {}", theme, e);
286       throw new ThemesServiceDatabaseException(e);
287     }
288   }
289 
290   private void updateTheme(Theme theme, ThemeDto themeDto) {
291     if (theme.getId().isPresent()) {
292       themeDto.setId(theme.getId().get());
293     }
294     themeDto.setUsername(theme.getCreator().getUsername());
295     themeDto.setCreationDate(theme.getCreationDate());
296     themeDto.setDefault(theme.isDefault());
297     themeDto.setName(theme.getName());
298     themeDto.setDescription(theme.getDescription());
299     themeDto.setBumperActive(theme.isBumperActive());
300     themeDto.setBumperFile(theme.getBumperFile());
301     themeDto.setTrailerActive(theme.isTrailerActive());
302     themeDto.setTrailerFile(theme.getTrailerFile());
303     themeDto.setTitleSlideActive(theme.isTitleSlideActive());
304     themeDto.setTitleSlideBackground(theme.getTitleSlideBackground());
305     themeDto.setTitleSlideMetadata(theme.getTitleSlideMetadata());
306     themeDto.setLicenseSlideActive(theme.isLicenseSlideActive());
307     themeDto.setLicenseSlideBackground(theme.getLicenseSlideBackground());
308     themeDto.setLicenseSlideDescription(theme.getLicenseSlideDescription());
309     themeDto.setWatermarkActive(theme.isWatermarkActive());
310     themeDto.setWatermarkFile(theme.getWatermarkFile());
311     themeDto.setWatermarkPosition(theme.getWatermarkPosition());
312   }
313 
314   @Override
315   public void deleteTheme(long id) throws ThemesServiceDatabaseException, NotFoundException {
316     try {
317       db.execTxChecked(em -> {
318         ThemeDto themeDto = getThemeDtoQuery(id).apply(em)
319             .orElseThrow(() -> new NotFoundException("No theme with id=" + id + " exists"));
320         namedQuery.remove(themeDto).accept(em);
321       });
322     } catch (NotFoundException e) {
323       throw e;
324     } catch (Exception e) {
325       logger.error("Could not delete theme '{}'", id, e);
326       throw new ThemesServiceDatabaseException(e);
327     }
328   }
329 
330   @Override
331   public int countThemes() throws ThemesServiceDatabaseException {
332     try {
333       String orgId = securityService.getOrganization().getId();
334       return db.exec(namedQuery.find(
335           "Themes.count",
336           Number.class,
337           Pair.of("org", orgId)
338       )).intValue();
339     } catch (Exception e) {
340       logger.error("Could not count themes", e);
341       throw new ThemesServiceDatabaseException(e);
342     }
343   }
344 
345   /**
346    * Gets a theme by its ID, using the current organizational context.
347    *
348    * @param id
349    *          the theme identifier
350    * @return a query function returning an optional theme entity
351    */
352   private Function<EntityManager, Optional<ThemeDto>> getThemeDtoQuery(long id) {
353     String orgId = securityService.getOrganization().getId();
354     return namedQuery.findOpt(
355         "Themes.findById",
356         ThemeDto.class,
357         Pair.of("id", id),
358         Pair.of("org", orgId)
359     );
360   }
361 }