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