View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2015 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.dam.assetservice.impl.dataversion;
21  
22  import java.util.Calendar;
23  import java.util.concurrent.ScheduledExecutorService;
24  import java.util.concurrent.TimeUnit;
25  
26  import javax.jcr.RepositoryException;
27  import javax.jcr.Session;
28  import javax.jcr.Value;
29  import javax.jcr.query.Query;
30  import javax.jcr.query.QueryManager;
31  import javax.jcr.query.QueryResult;
32  import javax.jcr.query.Row;
33  import javax.jcr.query.RowIterator;
34  
35  import org.apache.commons.lang3.builder.HashCodeBuilder;
36  import org.apache.commons.lang3.time.DateUtils;
37  import org.apache.commons.lang3.time.StopWatch;
38  import org.apache.jackrabbit.JcrConstants;
39  import org.apache.sling.api.resource.LoginException;
40  import org.apache.sling.api.resource.ResourceResolver;
41  import org.apache.sling.api.resource.ResourceResolverFactory;
42  
43  import com.day.cq.dam.api.DamConstants;
44  import com.day.cq.dam.api.DamEvent;
45  
46  import io.wcm.sling.commons.adapter.AdaptTo;
47  
48  /**
49   * Strategy that generates a checksum bases on all DAM asset's path and last modified dates within the DAM asset folder.
50   * The aggregated checksum is built by executing a JCR query using the AEM-predefined OAK index
51   * <code>damAssetLucene</code>. Executing the query does not touch the JCR content at all, it only reads
52   * JCR path and sha-1 string from the lucene index. This query is executed max. once during the "update interval",
53   * and only if DAM events occurred since the last checksum generation.
54   */
55  public class ChecksumDataVersionStrategy extends DataVersionStrategy {
56  
57    /**
58     * Data version strategy id for configuration persistence.
59     */
60    public static final String STRATEGY = "checksum";
61  
62    /**
63     * Default data version that is returned when no data version was yet calculated.
64     */
65    private static final String DATAVERSION_NOT_CALCULATED = "unknown";
66  
67    /**
68     * DAM asset property containing the last modified date.
69     */
70    private static final String LAST_MODIFIED_PROPERTY = JcrConstants.JCR_CONTENT + "/" + JcrConstants.JCR_LASTMODIFIED;
71  
72    private final long dataVersionUpdateIntervalMs;
73    private final String dataVersionQueryString;
74    private final ResourceResolverFactory resourceResolverFactory;
75  
76    private volatile String dataVersion;
77    private volatile long dataVersionLastUpdate;
78    private volatile long damEventLastOccurence;
79  
80    /**
81     * @param damPath DAM root path
82     * @param dataVersionUpdateIntervalSec Data version update interval
83     * @param resourceResolverFactory Resource resolver factory
84     * @param executor Shared executor service instance
85     */
86    public ChecksumDataVersionStrategy(String damPath,
87        int dataVersionUpdateIntervalSec,
88        ResourceResolverFactory resourceResolverFactory,
89        ScheduledExecutorService executor) {
90      super(damPath);
91  
92      this.dataVersionUpdateIntervalMs = dataVersionUpdateIntervalSec * DateUtils.MILLIS_PER_SECOND;
93      this.resourceResolverFactory = resourceResolverFactory;
94      this.dataVersionQueryString = buildDataVersionQueryString(damPath);
95  
96      this.dataVersion = DATAVERSION_NOT_CALCULATED;
97  
98      if (dataVersionUpdateIntervalSec <= 0) {
99        log.warn("{} - Invalid data version update interval: {} sec", damPath, dataVersionUpdateIntervalSec);
100     }
101     else {
102       Runnable task = new UpdateDataVersionTask();
103       executor.scheduleWithFixedDelay(task, 0, dataVersionUpdateIntervalSec, TimeUnit.SECONDS);
104     }
105   }
106 
107   /**
108    * Builds query string to fetch properties for all DAM assets lying in on of the configured DAM asset folders.
109    * @param damPath DAM root path
110    * @return SQL2 query string
111    */
112   private static String buildDataVersionQueryString(String damPath) {
113     return "select [" + JcrConstants.JCR_PATH + "], [" + LAST_MODIFIED_PROPERTY + "] "
114         + "from [" + DamConstants.NT_DAM_ASSET + "] as a "
115         + "where isdescendantnode(a, '" + damPath + "') "
116         + "order by [" + JcrConstants.JCR_PATH + "]";
117   }
118 
119   @Override
120   public void handleDamEvent(DamEvent damEvent) {
121     damEventLastOccurence = System.currentTimeMillis();
122   }
123 
124   @Override
125   public String getDataVersion() {
126     return dataVersion;
127   }
128 
129 
130   /**
131    * Scheduled task to generate a new data version via JCR query.
132    */
133   private class UpdateDataVersionTask implements Runnable {
134 
135     @Override
136     public void run() {
137       if (!isDataVersionStale()) {
138         log.debug("{} - Data version '{}' is not stale, skip generation of new data version.", damPath, dataVersion);
139         return;
140       }
141       try {
142         log.debug("{} - Data version '{}' is stale, start generation of new data version.", damPath, dataVersion);
143         generateDataVersion();
144       }
145       catch (LoginException ex) {
146         log.error("{} - Unable to get service resource resolver, please check service user configuration: {}", damPath, ex.getMessage());
147       }
148       /*CHECKSTYLE:OFF*/ catch (Exception ex) { /*CHECKSTYLE:ON*/
149         log.error("{} - Error generating data version: {}", damPath, ex.getMessage(), ex);
150       }
151     }
152 
153     private boolean isDataVersionStale() {
154       if (dataVersionLastUpdate == 0) {
155         return true;
156       }
157       // mark data version is stale if last update was after last DAM event
158       // add an additional interval's length to the comparison because the lucene is updated asynchronously
159       // and thus the DAM event may arrive before the updated properties are available in the index
160       return (dataVersionLastUpdate < damEventLastOccurence + dataVersionUpdateIntervalMs);
161     }
162 
163     /**
164      * Generates a data version by fetching all paths and properties from DAM asset folders (lucene index).
165      * The data version is a check sum over all path and selected properties found.
166      */
167     @SuppressWarnings("null")
168     private void generateDataVersion() throws LoginException, RepositoryException {
169       log.trace("{} - Start data version generation.", damPath);
170       ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(null);
171       try {
172         Session session = AdaptTo.notNull(resourceResolver, Session.class);
173         QueryManager queryManager = session.getWorkspace().getQueryManager();
174         Query query = queryManager.createQuery(dataVersionQueryString, Query.JCR_SQL2);
175         QueryResult result = query.execute();
176         RowIterator rows = result.getRows();
177 
178         HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
179         int assetCount = 0;
180         StopWatch stopwatch = new StopWatch();
181         stopwatch.start();
182 
183         while (rows.hasNext()) {
184           Row row = rows.nextRow();
185           String path = getStringValue(row, JcrConstants.JCR_PATH);
186           Calendar lastModified = getCalendarValue(row, LAST_MODIFIED_PROPERTY);
187           log.trace("{} - Found sha-1 at {}: {}", damPath, path, lastModified);
188 
189           hashCodeBuilder.append(path);
190           if (lastModified != null) {
191             hashCodeBuilder.append(lastModified);
192           }
193           else {
194             log.debug("{} - No last modified date found for {}", damPath, path);
195           }
196           assetCount++;
197         }
198 
199         dataVersion = Integer.toString(hashCodeBuilder.build());
200         dataVersionLastUpdate = System.currentTimeMillis();
201 
202         stopwatch.stop();
203         log.info("{} - Generated new data version {} for {} assets (duration: {}ms).",
204             damPath, dataVersion, assetCount, stopwatch.getTime());
205       }
206       finally {
207         resourceResolver.close();
208       }
209     }
210 
211     private String getStringValue(Row row, String property) throws RepositoryException {
212       Value value = row.getValue(property);
213       if (value != null) {
214         return value.getString();
215       }
216       else {
217         return null;
218       }
219     }
220 
221     private Calendar getCalendarValue(Row row, String property) throws RepositoryException {
222       Value value = row.getValue(property);
223       if (value != null) {
224         return value.getDate();
225       }
226       else {
227         return null;
228       }
229     }
230 
231   }
232 
233 }