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