View Javadoc

1   /*
2    * Copyright 2008 FatWire Corporation. All Rights Reserved.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.fatwire.gst.foundation.facade.mda;
18  
19  import java.util.Arrays;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.List;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  
27  import COM.FutureTense.Interfaces.FTValList;
28  import COM.FutureTense.Interfaces.ICS;
29  import COM.FutureTense.Interfaces.IList;
30  import COM.FutureTense.Util.IterableIListWrapper;
31  import COM.FutureTense.Util.ftErrors;
32  
33  import com.fatwire.assetapi.data.AssetId;
34  import com.fatwire.cs.core.db.PreparedStmt;
35  import com.fatwire.cs.core.db.StatementParam;
36  import com.fatwire.gst.foundation.CSRuntimeException;
37  import com.fatwire.gst.foundation.IListUtils;
38  import com.fatwire.gst.foundation.facade.ics.ICSFactory;
39  import com.fatwire.gst.foundation.facade.runtag.asset.AssetLoadByName;
40  import com.fatwire.gst.foundation.facade.runtag.render.LogDep;
41  import com.fatwire.mda.Dimension;
42  import com.fatwire.mda.DimensionException;
43  import com.fatwire.mda.DimensionFilterInstance;
44  import com.fatwire.mda.DimensionManager;
45  import com.fatwire.mda.DimensionSetInstance;
46  import com.fatwire.mda.DimensionableAssetManager;
47  import com.fatwire.system.Session;
48  import com.openmarket.xcelerate.asset.AssetIdImpl;
49  
50  /**
51   * Handles Locale-specific functions efficiently in Java.
52   * 
53   * @author Tony Field
54   * @since May 8, 2009
55   */
56  
57  public final class LocaleUtils {
58      private static final Log _log = LogFactory.getLog(LocaleUtils.class);
59  
60      private LocaleUtils() {
61      }
62  
63      /**
64       * Look up the translation for the asset specified, in the locale specified.
65       * <p/>
66       * If the desired translation is not available, null will be returned.
67       * <p/>
68       * If a dimension set for the site has been configured that returns the
69       * asset other than the preferred locale, that is considered to be fine and
70       * not really the problem of the end user. In other words, a dimension set
71       * may dictate that a "backup" language can be returned to the user.
72       * <p/>
73       * Null, however, is a valid option.
74       * 
75       * @param c asset type of asset to look up
76       * @param cid asset id of asset to look up
77       * @param preferredLocaleDimensionId id of locale desired
78       * @param site name of site
79       * @return AssetId of translation asset.
80       * @deprecated Use #findTranslation(ICS,String,String,String,String)
81       *             instead. This version is unable to correctly record
82       *             compositional dependencies on the DimensionSet asset for the
83       *             site, which can result in the translation function not
84       *             flushing pages when the dimension set is altered (for
85       *             example, to change the filter, or change enabled dimensions).
86       */
87      public static AssetId findTranslation(String c, String cid, String preferredLocaleDimensionId, String site) {
88          return findTranslation(ICSFactory.getOrCreateICS(), new AssetIdImpl(c, Long.valueOf(cid)),
89                  preferredLocaleDimensionId, site);
90      }
91  
92      /**
93       * Look up the translation for the asset specified, in the locale specified.
94       * <p/>
95       * If the desired translation is not available, null will be returned.
96       * <p/>
97       * If a dimension set for the site has been configured that returns the
98       * asset other than the preferred locale, that is considered to be fine and
99       * not really the problem of the end user. In other words, a dimension set
100      * may dictate that a "backup" language can be returned to the user.
101      * <p/>
102      * Null, however, is a valid option.
103      * 
104      * @param c asset type of asset to look up
105      * @param cid asset id of asset to look up
106      * @param preferredLocaleDimensionId id of locale desired
107      * @param site name of site
108      * @param ics ics context
109      * @return AssetId of translation asset.
110      */
111     public static AssetId findTranslation(ICS ics, String c, String cid, String preferredLocaleDimensionId, String site) {
112         return findTranslation(ics, new AssetIdImpl(c, Long.valueOf(cid)), preferredLocaleDimensionId, site);
113     }
114 
115     /**
116      * Look up the translation for the asset specified, in the locale specified.
117      * <p/>
118      * If the desired translation is not available, null will be returned.
119      * <p/>
120      * If a dimension set for the site has been configured that returns the
121      * asset other than the preferred locale, that is considered to be fine and
122      * not really the problem of the end user. In other words, a dimension set
123      * may dictate that a "backup" language can be returned to the user.
124      * <p/>
125      * Null, however, is a valid option.
126      * 
127      * @param id id of asset to look up
128      * @param preferredLocaleDimensionIdString id of locale desired
129      * @param site name of site
130      * @return AssetId of translation asset, or null if none is returned by the
131      *         dimension set filter.
132      * @deprecated Use #findTranslation(ICS,AssetId,String,String) instead. This
133      *             version is unable to correctly record compositional
134      *             dependencies on the DimensionSet asset for the site, which
135      *             can result in the translation function not flushing pages
136      *             when the dimension set is altered (for example, to change the
137      *             filter, or change enabled dimensions).
138      */
139     public static AssetId findTranslation(AssetId id, String preferredLocaleDimensionIdString, String site) {
140         ICS ics = ICSFactory.getOrCreateICS();
141         if (preferredLocaleDimensionIdString == null) {
142             throw new IllegalArgumentException("Required preferred locale dimension ID not provided");
143         }
144         long preferredDimension = Long.valueOf(preferredLocaleDimensionIdString);
145 
146         long dimensionSetId = locateDimensionSetForSite(ics, site);
147 
148         return findTranslation(ics, id, preferredDimension, dimensionSetId);
149     }
150 
151     /**
152      * Look up the translation for the asset specified, in the locale specified.
153      * <p/>
154      * If the desired translation is not available, null will be returned.
155      * <p/>
156      * If a dimension set for the site has been configured that returns the
157      * asset other than the preferred locale, that is considered to be fine and
158      * not really the problem of the end user. In other words, a dimension set
159      * may dictate that a "backup" language can be returned to the user.
160      * <p/>
161      * Null, however, is a valid option.
162      * 
163      * @param ics context
164      * @param id id of asset to look up
165      * @param preferredLocaleDimensionIdString id of locale desired
166      * @param site name of site
167      * @return AssetId of translation asset, or null if none is returned by the
168      *         dimension set filter.
169      */
170     public static AssetId findTranslation(ICS ics, AssetId id, String preferredLocaleDimensionIdString, String site) {
171         if (preferredLocaleDimensionIdString == null) {
172             throw new IllegalArgumentException("Required preferred locale dimension ID not provided");
173         }
174         long preferredDimension = Long.valueOf(preferredLocaleDimensionIdString);
175 
176         long dimensionSetId = locateDimensionSetForSite(ics, site);
177 
178         return findTranslation(ics, id, preferredDimension, dimensionSetId);
179     }
180 
181     /**
182      * Look up the translation for the asset specified, in the locale specified.
183      * <p/>
184      * If the desired translation is not available, null will be returned.
185      * <p/>
186      * If a dimension set for the site has been configured that returns the
187      * asset other than the preferred locale, that is considered to be fine and
188      * not really the problem of the end user. In other words, a dimension set
189      * may dictate that a "backup" language can be returned to the user.
190      * <p/>
191      * Null, however, is a valid option.
192      * 
193      * @param id id of asset to look up
194      * @param preferredDimension id of locale desired
195      * @param dimensionSetId dimension set to use to find the translation
196      * @return AssetId of translation asset, or null if none is returned by the
197      *         dimension set filter.
198      */
199     public static AssetId findTranslation(ICS ics, AssetId id, long preferredDimension, long dimensionSetId) {
200         if (id == null) {
201             throw new IllegalArgumentException("Required Asset ID missing");
202         }
203 
204         DimensionableAssetManager mgr = DimensionUtils.getDAM(ics);
205 
206         if (_isInputAssetDimensionPreferred(mgr, id, preferredDimension)) {
207             _log.debug("Input dimension is already in the preferred dimension.  Not invoking dimension set filter.  Asset: "
208                     + id + ", dimension: " + preferredDimension);
209             return id;
210         } else {
211             _log.debug("About to look for translations.  Input asset id: " + id + ", dimension set: " + dimensionSetId
212                     + ", preferred dimension: " + preferredDimension);
213         }
214 
215         // *****************************************************************************
216         // The core business logic of this helper class is encapsulated in these
217         // 3 lines
218         DimensionSetInstance dimset = getDimensionSet(ics, dimensionSetId);
219         return findTranslation(ics, id, preferredDimension, dimset);
220 
221     }
222 
223     /**
224      * Look up the translation for the asset specified, in the locale specified.
225      * <p/>
226      * If the desired translation is not available, null will be returned.
227      * <p/>
228      * If a dimension set has been configured that returns the asset other than
229      * the preferred locale, that is considered to be fine and not really the
230      * problem of the end user. In other words, a dimension set may dictate that
231      * a "backup" language can be returned to the user.
232      * <p/>
233      * Null, however, is a valid option.
234      * 
235      * @param id id of asset to look up
236      * @param preferredDimension id of locale desired
237      * @param dimensionSetName the name of the dimension set to use to find the
238      *            translation
239      * @return AssetId of translation asset, or null if none is returned by the
240      *         dimension set filter. The id parameters is returned if the asset
241      *         does not have a locale or if the locale is already of the
242      *         preferredDimension
243      */
244     public static AssetId findTranslation(ICS ics, AssetId id, long preferredDimension, String dimensionSetName) {
245         if (id == null) {
246             throw new IllegalArgumentException("Required Asset ID missing");
247         }
248         Dimension locale = DimensionUtils.getLocaleAsDimension(ics, id);
249 
250         if (locale == null) {
251             _log.debug("Asset is not localized.  Not invoking dimension set filter.  Asset: " + id);
252             return id;
253         }
254         if (locale.getId().getId() == preferredDimension) {
255             _log.debug("Input dimension is already in the preferred dimension.  Not invoking dimension set filter.  Asset: "
256                     + id + ", dimension: " + preferredDimension);
257             return id;
258         } else {
259             _log.debug("About to look for translations.  Input asset id: " + id + ", dimension set: "
260                     + dimensionSetName + ", preferred dimension: " + preferredDimension);
261         }
262 
263         // *****************************************************************************
264         // The core business logic of this helper class is encapsulated in these
265         // 3 lines
266         DimensionSetInstance dimset = getDimensionSet(ics, dimensionSetName);
267 
268         return findTranslation(ics, id, preferredDimension, dimset);
269     }
270 
271     /**
272      * @param ics
273      * @param id
274      * @param preferredDimension
275      * @param dimset
276      * @return assetid of translated asset.
277      * @throws IllegalStateException
278      */
279     public static AssetId findTranslation(ICS ics, AssetId id, long preferredDimension, DimensionSetInstance dimset)
280             throws IllegalStateException {
281         AssetId preferredDim = new AssetIdImpl("Dimension", preferredDimension);
282         List<AssetId> preferredDims = Collections.singletonList(preferredDim);
283         Collection<AssetId> relatives = findTranslation(DimensionUtils.getDM(ics), Collections.singletonList(id), preferredDims, dimset );
284         // *****************************************************************************
285 
286         // make the result pretty
287         if (relatives == null) {
288             _log.warn("No translation found for asset " + id + " in dimension set " + dimset.getId()
289                     + " for dimension " + preferredDimension + ".");
290             return null;
291         } else {
292             switch (relatives.size()) {
293                 case 0: {
294                     _log.warn("No translation found for " + id + " in dimension set " + dimset.getId()
295                             + " for dimension " + preferredDimension + ".");
296                     // Note May 4, 2010 by Tony Field - this had been changed to
297                     // return the input ID but that
298                     // is incorrect. The contract clearly states that null is to
299                     // be returned if no matching
300                     // relatives are found. When null is returned and it is not
301                     // expected, often the incorrect
302                     // dimension filter is configured.
303                     return null;
304                 }
305                 case 1: {
306                     AssetId relative = relatives.iterator().next();
307                     _log.trace("LocaleUtils.findTranslation: RELATIVE FOUND... " + relative.getType() + " '"
308                             + relative.getId() + "' // errno = " + ics.GetErrno());
309                     return relative;
310 
311                 }
312                 default: {
313                     throw new IllegalStateException("found more than one translation for asset " + id
314                             + " and that is not supposed to be possible.");
315                 }
316             }
317         }
318     }
319 
320 
321     /**
322      * Main translation lookup method.  Accesses the filter in the dimension set, configures it with the preferred
323      * dimension IDs, then filters the input assets.
324      * @param dimensionManager manager class for Dimension lookups
325      * @param toFilterList list of input assets that need to be translated.  Often it's just one, but a list is perfectly valid.
326      * @param preferredDimensionIds preferred dimensions to be investigated for a result. Priority preference depends on the
327      * configured filter
328      * @param dimSet DimensionSet to use for filtering.
329      * @return list of assets based on the translation rules in the dimension filter from the specified dimension set.
330      *
331      */
332     public static Collection<AssetId> findTranslation(DimensionManager dimensionManager, List<AssetId> toFilterList, Collection<AssetId> preferredDimensionIds, DimensionSetInstance dimSet) {
333         try {
334             return DimensionUtils.filterAssets(dimensionManager, toFilterList, preferredDimensionIds, dimSet);
335         } catch (DimensionException e) {
336             throw new CSRuntimeException("Failed to translate assets.  Input assets:"+toFilterList+", Preferred Dimensions: "+preferredDimensionIds+", DimensionSet:"+dimSet, ftErrors.exceptionerr, e);
337         }
338     }
339 
340     // ///////////////////////////////////////////////////////////////////////////
341     // Helper functions
342 
343     private static boolean _isInputAssetDimensionPreferred(DimensionableAssetManager mgr, AssetId id,
344             long preferredDimension) {
345         Dimension dim = DimensionUtils.getLocaleAsDimension(mgr, id);
346         if (dim == null)
347             return true; // if locale not found, tell that the asset is expected
348                          // locale
349         return dim.getId().getId() == preferredDimension;
350     }
351 
352     private static final PreparedStmt FIND_DIMSET_FOR_SITE_PREPAREDSTMT = new PreparedStmt(
353             "select ds.id as id from DimensionSet ds, Publication p, AssetPublication ap where p.name = ? and p.id = ap.pubid and ap.assetid = ds.id and ds.status != 'VO' order by ds.updateddate",
354             Arrays.asList("DimensionSet", "AssetPublication", "Publication"));
355 
356     static {
357         FIND_DIMSET_FOR_SITE_PREPAREDSTMT.setElement(0, "Publication", "name");
358     }
359 
360     /**
361      * Locates a single dimension set in a site. If no match is found, an
362      * exception is thrown. If more than one match is found, an exception is
363      * thrown.
364      * 
365      * @param ics context
366      * @param site site containing a dimension set
367      * @return DimensionSet ID
368      */
369     public static long locateDimensionSetForSite(ICS ics, String site) {
370         if (site == null) {
371             throw new IllegalArgumentException("Required site name missing");
372         }
373         StatementParam params = FIND_DIMSET_FOR_SITE_PREPAREDSTMT.newParam();
374         params.setString(0, site);
375         IList results = ics.SQL(FIND_DIMSET_FOR_SITE_PREPAREDSTMT, params, true);
376         int numRows = results != null && results.hasData() ? results.numRows() : 0;
377         if (numRows == 0) {
378             throw new IllegalStateException(
379                     "A DimensionSet has not been defined for site '"
380                             + site
381                             + "'. Cannot determine any translation unless some locales (Dimensions) are enabled for that site. Aborting operation.");
382         }
383         if (numRows > 1) {
384             String msg = "More than one dimension set found in site " + site
385                     + ".  Exactly one is expected.  Dimension set ids: ";
386             for (IList row : new IterableIListWrapper(results)) {
387                 String id = IListUtils.getStringValue(row, "id");
388                 LogDep.logDep(ics, "DimensionSet", id);
389                 msg += id + " ";
390             }
391             throw new IllegalStateException(msg + ".");
392         }
393         results.moveTo(1);
394         String id = IListUtils.getStringValue(results, "id");
395         LogDep.logDep(ics, "DimensionSet", id);
396         if (_log.isTraceEnabled()) _log.trace("Looked up dimset for site "+site+" and found "+id);
397         return Long.valueOf(id);
398     }
399 
400     private static DimensionFilterInstance _getPopulatedDimensionFilter(Session ses, DimensionSetInstance dimset,
401             long localeDimensionId) {
402 
403         // Set the filter's preferred dimension
404         // Equivalent to:
405         // %><dimensionset:asset assettype="Dimension"
406         // assetid="<%=localeDimensionId%>" /><%
407         Dimension thePreferredDimension = ((DimensionManager) ses.getManager(DimensionManager.class.getName()))
408                 .loadDimension(localeDimensionId);
409         if (thePreferredDimension == null) {
410             throw new RuntimeException("Attempted to load Dimension with id " + localeDimensionId
411                     + " but it came back null");
412         }
413         return _getPopulatedDimensionFilter(dimset, thePreferredDimension);
414     }
415 
416     private static DimensionFilterInstance _getPopulatedDimensionFilter(DimensionSetInstance dimset,
417             Dimension preferredDimension) {
418         DimensionFilterInstance filter;
419         try {
420             filter = dimset.getFilter();
421         } catch (DimensionException e) {
422             throw new RuntimeException("Could not get Dimension Filter from DimensionSet", e);
423         }
424 
425         filter.setDimensonPreference(Collections.singletonList(preferredDimension));
426         return filter;
427     }
428 
429     public static DimensionSetInstance getDimensionSet(ICS ics, long theDimSetId) {
430         final String DIMSET_OBJ_NAME = "LocaleUtils:findTranslation:theDimensionSet:DimensionSet";
431 
432         // Load the site-specific DimensionSet asset
433         ics.SetObj(DIMSET_OBJ_NAME, null); // clear first
434         FTValList args = new FTValList();
435         args.put("NAME", DIMSET_OBJ_NAME);
436         args.put("TYPE", "DimensionSet");
437         args.put("OBJECTID", Long.toString(theDimSetId));
438         args.put("EDITABLE", "FALSE");
439         ics.runTag("ASSET.LOAD", args);
440 
441         if (ics.GetErrno() < 0) {
442             throw new IllegalStateException("Could not load dimension set.  Errno: " + ics.GetErrno());
443         }
444 
445         Object o = ics.GetObj(DIMSET_OBJ_NAME);
446         if (o == null) {
447             throw new IllegalStateException("Could not load dimension set but we got no errno... unexpected...");
448         }
449 
450         DimensionSetInstance dimset;
451         if (o instanceof DimensionSetInstance) {
452             dimset = (DimensionSetInstance) o;
453         } else {
454             throw new IllegalStateException("Loaded an asset that is not a Dimension Set");
455         }
456         return dimset;
457     }
458 
459     public static DimensionSetInstance getDimensionSet(ICS ics, String name) {
460         final String DIMSET_OBJ_NAME = "LocaleUtils:findTranslation:theDimensionSet:DimensionSet";
461         ics.SetObj(DIMSET_OBJ_NAME, null);
462         AssetLoadByName a = new AssetLoadByName();
463         a.setAssetType("DimensionSet");
464         a.setAssetName(name);
465         a.setEditable(false);
466         a.setName(DIMSET_OBJ_NAME);
467         a.execute(ics);
468 
469         if (ics.GetErrno() < 0) {
470             throw new IllegalStateException("Could not load dimension set.  Errno: " + ics.GetErrno());
471         }
472 
473         Object o = ics.GetObj(DIMSET_OBJ_NAME);
474         ics.SetObj(DIMSET_OBJ_NAME, null);
475         if (o == null) {
476             throw new IllegalStateException("Could not load dimension set but we got no errno... unexpected...");
477         }
478 
479         DimensionSetInstance dimset;
480         if (o instanceof DimensionSetInstance) {
481             dimset = (DimensionSetInstance) o;
482         } else {
483             throw new IllegalStateException("Loaded an asset that is not a Dimension Set");
484         }
485         return dimset;
486     }
487 
488 }