/* * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package com.arsdigita.cms; import com.arsdigita.categorization.Category; import com.arsdigita.categorization.CategoryCollection; import com.arsdigita.cms.lifecycle.Lifecycle; import com.arsdigita.cms.lifecycle.LifecycleDefinition; import com.arsdigita.cms.util.LanguageUtil; import com.arsdigita.domain.AbstractDomainObjectObserver; import com.arsdigita.domain.DataObjectNotFoundException; import com.arsdigita.domain.DomainObject; import com.arsdigita.domain.DomainObjectFactory; import com.arsdigita.domain.DomainObjectObserver; import com.arsdigita.globalization.GlobalizationHelper; import com.arsdigita.kernel.Kernel; import com.arsdigita.kernel.permissions.PermissionService; import com.arsdigita.persistence.DataAssociation; import com.arsdigita.persistence.DataAssociationCursor; import com.arsdigita.persistence.DataObject; import com.arsdigita.persistence.OID; import com.arsdigita.persistence.metadata.Property; import com.arsdigita.util.Assert; import com.arsdigita.web.Web; import com.arsdigita.workflow.simple.Workflow; import com.arsdigita.workflow.simple.WorkflowTemplate; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Locale; import org.apache.log4j.Logger; /** * A bundle of content items of different languages. A bundle ties * the various language instances of an item together and provides * methods to access them. * * @author Shashin Shinde * @author Justin Ross <jross@redhat.com> * @version $Id: ContentBundle.java 2273 2012-01-20 00:21:50Z pboy $ */ public class ContentBundle extends ContentItem { private static final Logger s_log = Logger.getLogger(ContentBundle.class); private static DomainObjectObserver s_instancesObserver = new AbstractDomainObjectObserver() { @Override public void add(DomainObject dom, String name, DataObject dobj) { if (INSTANCES.equals(name)) { if (dobj != null) { PermissionService.setContext(dobj.getOID(), dom.getOID()); } } } }; /** * The base data object type of a bundle */ public static final String BASE_DATA_OBJECT_TYPE = "com.arsdigita.cms.ContentBundle"; /** * The primary instances association */ public static final String INSTANCES = "instances"; /** * The association to AtoZ aliases */ public static final String ATOZ_ALIASING_PROVIDERS = "atozAliasingProviders"; /** * The default language property */ public static final String DEFAULT_LANGUAGE = "defaultLanguage"; private boolean m_wasNew = false; /** * Returns the data object type for this bundle. */ @Override public String getBaseDataObjectType() { return BASE_DATA_OBJECT_TYPE; } /** * Creates a new bundle. * * @param primary The primary language instance of this bundle */ public ContentBundle(final ContentItem primary) { super(BASE_DATA_OBJECT_TYPE); Assert.exists(primary, ContentItem.class); setDefaultLanguage(primary.getLanguage()); setContentType(primary.getContentType()); addInstance(primary); super.setName(primary.getName()); } /** * Retrieves a bundle. * * @param oid the OID of the bundle to retrieve */ public ContentBundle(final OID oid) throws DataObjectNotFoundException { super(oid); } /** * Retrieves a bundle. * * @param id the BigDecimal id of the bundle to * retrieve */ public ContentBundle(final BigDecimal id) throws DataObjectNotFoundException { this(new OID(BASE_DATA_OBJECT_TYPE, id)); } /** * Retrieves or creates a bundle using the DataObject * argument. * * @param object the DataObject to use in creating or * retrieving the bundle */ public ContentBundle(final DataObject object) { super(object); } /** * Creates a bundle. * * @param type the String data object type with which * to create a new bundle */ public ContentBundle(final String type) { super(type); } @Override protected ContentItem makeCopy() { final ContentBundle newItem = (ContentBundle) super.makeCopy(); final WorkflowTemplate template = ContentTypeWorkflowTemplate.getWorkflowTemplate(newItem.getContentSection(), newItem.getContentType()); if (template != null) { s_log.debug("Setting up new workflow template"); ItemCollection instances = getInstances(); while (instances.next()) { ContentItem instance = instances.getContentItem(); s_log.debug("Item id is: " + instance.getID()); final Workflow workflow = template.instantiateNewWorkflow(); workflow.setObjectID(instance.getID()); workflow.start(Web.getWebContext().getUser()); workflow.save(); } } return newItem; } /** * Gets the default language of the bundle. */ public final String getDefaultLanguage() { return (String) get(DEFAULT_LANGUAGE); } /** * Sets the default language of the bundle. */ public final void setDefaultLanguage(final String language) { if (Assert.isEnabled()) { Assert.exists(language, String.class); Assert.isTrue(language.length() == 2, language + " is not an ISO639 language code"); } set(DEFAULT_LANGUAGE, language); } /** * Adds a language instance to this bundle. This method will fail * if the bundle already contains a different instance for the * same language. * * Note that in order to set the primary instance you must call * this method and {@link #setDefaultLanguage(String)} as well. * * @param instance the new language instance * @see #setDefaultLanguage(String) * @pre instance != null * @post this.equals(instance.getParent()) */ public void addInstance(final ContentItem instance) { if (s_log.isDebugEnabled()) { s_log.debug("Adding " + instance + " to bundle " + this); } if (Assert.isEnabled()) { Assert.exists(instance, ContentItem.class); Assert.isFalse(hasInstance(instance.getLanguage()), "The bundle already contains an instance " + "for the language " + instance.getLanguage()); } instance.setParent(this); instance.setContentSection(getContentSection()); if (Assert.isEnabled()) { Assert.isEqual(this, instance.getParent()); } } /** * Removes a language instance from the bundle. This method will * fail if instance is the primary instance. * * Note that the language instance is not deleted by this * operation; it is just removed from the Bundle. Users of this * method have to take care to properly dispose of this instance! * * @param instance The language instance to remove * @pre instance != null * @post instance.getParent() == null */ public void removeInstance(final ContentItem instance) { if (Assert.isEnabled()) { Assert.exists(instance, ContentItem.class); Assert.isEqual(this, instance.getParent()); Assert.isNotEqual(instance, getPrimaryInstance()); } instance.setParent(null); if (Assert.isEnabled()) { Assert.isTrue(instance.getParent() == null); } } /** * Gets the primary instance of this bundle. * * @return the language instance of this item which is marked as * the primary instance * @see #addInstance(ContentItem) */ public final ContentItem getPrimaryInstance() { return getInstance(getDefaultLanguage()); } /** * Produces a collection containing all language instances in this * bundle. * * @return a collection of language instances */ public final ItemCollection getInstances() { return new ItemCollection(instances()); } public final ContentItem getInstance(final Locale locale) { return this.getInstance(locale.getLanguage(), false); } public final ContentItem getInstance(final Locale locale, boolean allowLanguageIndependent) { return this.getInstance(locale.getLanguage(), allowLanguageIndependent); } /** * Returns a language instance for language or * null if no such instance exists. * * This method does not do language negotiation, * it only returns an exact match for the given Locale or * null if no such match is found. * * It will try to return a language independent version of the * content item, if there is one and {@code allowLanguageIndependent} * is true. * * @param language the language for which to get an instance * @return the instance of this item which exactly matches the * language part of the Locale l * @see #negotiate * @pre language != null */ public final ContentItem getInstance(final String language) { return this.getInstance(language, Kernel.getConfig().languageIndependentItems()); } public final ContentItem getInstance(final String language, boolean allowLanguageIndependent) { if (Assert.isEnabled()) { Assert.exists(language, String.class); Assert.isTrue(language.length() == 2, language + " does not look like a valid language " + "code"); } // The data object to return ContentItem contentItem = null; // Try to get the content item in the exact language DataAssociationCursor instances = instances(); instances.addEqualsFilter(LANGUAGE, language); if (instances.next()) { contentItem = (ContentItem) DomainObjectFactory.newInstance(instances.getDataObject()); } instances.close(); // Try to get a language independent version of the content item, // if we couldn't find an exact match and language independent // content items are acceptable. if (contentItem == null && allowLanguageIndependent == true) { contentItem = this.getInstance(GlobalizationHelper.LANG_INDEPENDENT, false); } return contentItem; } /** * Tells whether instance is present in the bundle. * * @param instance the language instance to look for * @return true if the instance is in the bundle */ public final boolean hasInstance(final ContentItem instance) { Assert.exists(instance, ContentItem.class); final DataAssociationCursor instances = instances(); instances.addEqualsFilter(ID, instance.getID()); return !instances.isEmpty(); } /** * Utility method to check if this bundle already contains an * instance for the given language. * * @param language an ISO639 2-letter language code * @return true if this ContentBundle * contains an instance for the language given as an argument * @see ContentItem#getLanguage() */ public final boolean hasInstance(final String language) { return this.hasInstance(language, false); } public final boolean hasInstance(final String language, boolean allowLanguageIndependent) { if (Assert.isEnabled()) { Assert.exists(language, String.class); Assert.isTrue(language.length() == 2, language + " is not an ISO639 language code"); } final DataAssociationCursor instances = instances(); // If allowLanguageIndependent == false (default case), only search // for an exact language match if (allowLanguageIndependent == false) { instances.addEqualsFilter(LANGUAGE, language); } // Else, search also for language independent version else { /*FilterFactory ff = instances.getFilterFactory(); instances.addFilter( ff.or().addFilter(ff.equals(LANGUAGE, language)). addFilter(ff.equals(LANGUAGE, "--")));*/ instances.addFilter(String.format("(%s = '%s' or %s = '%s')", LANGUAGE, language, LANGUAGE, GlobalizationHelper.LANG_INDEPENDENT)); } return !instances.isEmpty(); } /** * List all languages in which this item is available, i.e. the * language codes of all instances in this bundle. * * @return A Collection of language 2-letter codes in * which this item is available */ public final Collection getLanguages() { // XXX For LIVE bundles, there might be several PENDING // instances with the same language. Maybe we should filter // these out and return only one? final ItemCollection items = getInstances(); final Collection list = new ArrayList(); while (items.next()) { list.add(items.getLanguage()); } items.close(); if (Assert.isEnabled()) { Assert.isTrue(!list.isEmpty() || getInstances().isEmpty()); } return list; } /** * Negotiate the right language instance for this bundle and return it. * * @param locales the acceptable locales for the language * instance, in decreasing importance * @return the negotiated language instance or null * if there is no language instance for any of the locales in * locales * @pre locales != null * @deprecated Locale negotiation takes place in * {@link com.arsdigita.globalization.GlobalizationHelper}. * Use {@link #getInstance(java.lang.String)} instead. */ // Quasimodo: // Is this method ever used? Netbeans couldn't find anything. @Deprecated public ContentItem negotiate(Locale[] locales) { Assert.exists(locales); String supportedLanguages = LanguageUtil.getSupportedLanguages(); DataAssociationCursor instancesCursor = instances(); DataObject dataObject = null; int bestMatch = 0; DataObject matchingInstance = null; String language = null; while (instancesCursor.next()) { dataObject = instancesCursor.getDataObject(); language = (String) dataObject.get(LANGUAGE); // If language is not one of the supported languages, skip this entry if (!supportedLanguages.contains(language)) { continue; } if (s_log.isDebugEnabled()) { s_log.debug("negotiate: language= " + language); } if (language != null) { // If the current object is languange independent and no better // match is already found, match it with the lowest priority if (language.equals("--") && matchingInstance == null) { bestMatch = locales.length; matchingInstance = dataObject; } else { // In any other case for (int i = 0; i < locales.length; i++) { if (language.equals(locales[i].getLanguage())) { if (i < bestMatch || matchingInstance == null) { bestMatch = i; matchingInstance = dataObject; if (s_log.isDebugEnabled()) { s_log.debug("negotiate: " + "bestMatch= " + i + ", language= " + language); } } // else other match with less preferred language found } } // end for } } // end if if (bestMatch == 0 && matchingInstance != null) { s_log.debug("negotiate: best possible match found, exiting"); break; // exit loop when best match is found } } instancesCursor.close(); if (matchingInstance != null) { return (ContentItem) DomainObjectFactory.newInstance(matchingInstance); } else { s_log.info("negotiate: no match found!"); return null; } } /** * Negotiate the right language instance for this bundle and return it. * * @param locales the acceptable locales for the language instance, in * decreasing importance. This parameter has to be an * Enumeration of Locale objects. * @return the negotiated language instance or null if there * is no language instance for any of the locales in locales. * @pre locales != null * @deprecated Locale negotiation takes place in * {@link com.arsdigita.globalization.GlobalizationHelper}. * Use {@link #getInstance(java.lang.String)} instead. */ @Deprecated public ContentItem negotiate(Enumeration locales) { String supportedLanguages = LanguageUtil.getSupportedLanguages(); Assert.exists(locales); /* copy "locales" enumeration, since we have to iterate * over it several times */ Locale loc = null; List languageCodes = new ArrayList(); for (int i = 0; locales.hasMoreElements(); i++) { loc = (Locale) locales.nextElement(); // Quasimodo: // Only add languages to the List which are supported by cms if (supportedLanguages.contains(loc.getLanguage())) { languageCodes.add(loc.getLanguage()); } if (s_log.isDebugEnabled()) { s_log.debug("negotiate: pref " + i + ": " + loc.getLanguage()); } } // Add unspecified language for language independent objects if (supportedLanguages.contains("--")) { languageCodes.add("--"); } final DataAssociationCursor instances = instances(); DataObject dataObject = null; int bestMatch = 0; DataObject match = null; String language = null; while (instances.next()) { dataObject = instances.getDataObject(); language = (String) dataObject.get(LANGUAGE); if (s_log.isDebugEnabled()) { s_log.debug("negotiate: language= " + language); } if (language != null) { for (int i = 0; i < languageCodes.size(); i++) { if (language.equals((String) languageCodes.get(i))) { if (i < bestMatch || match == null) { bestMatch = i; match = dataObject; if (s_log.isDebugEnabled()) { s_log.debug("negotiate: " + "bestMatch= " + i + ", language= " + language); } } // else other match with less preferred language found } } // end for } // end if if (bestMatch == 0 && match != null) { s_log.debug("negotiate: best possible match found, exiting"); break; // exit loop when best match is found } } instances.close(); return (ContentItem) DomainObjectFactory.newInstance(match); } // Methods from item that bundle overrides @Override protected void beforeSave() { super.beforeSave(); final ContentItem primary = getPrimaryInstance(); Assert.exists(getContentType(), ContentType.class); if (primary != null) { primary.setContentSection(getContentSection()); } } @Override protected boolean canPublishToFS() { return false; } @Override protected void publishToFS() { throw new UnsupportedOperationException(); } @Override public ContentItem publish(final LifecycleDefinition definition, final Date start) { throw new UnsupportedOperationException(); } @Override public Lifecycle getLifecycle() { // Bundles do not have lifecycles. return null; } @Override public void setLifecycle(final Lifecycle lifecycle) { // I'd like to do the following, but VersionCopier calls // setLifecycle. //throw new UnsupportedOperationException(); } /** * Ignore the INSTANCES property for * ItemCopier.VERSION_COPY. * * @param source the source CustomCopy item * @param property the property to copy * @param copier a temporary class that is able to copy a child item * correctly. * @return true if the property was copied; false to indicate * that regular metadata-driven methods should be used * to copy the property. */ @Override public boolean copyProperty(final CustomCopy source, final Property property, final ItemCopier copier) { if (copier.getCopyType() == ItemCopier.VERSION_COPY) { if (INSTANCES.equals(property.getName())) { return true; } else if (ATOZ_ALIASING_PROVIDERS.equals(property.getName())) { return true; } } return super.copyProperty(source, property, copier); } @Override public boolean copyServices(final ContentItem source) { if (s_log.isDebugEnabled()) { s_log.debug("Copying services on bundle " + getName() + " " + getID() + " using source " + source.getID()); } // Copy categories CategoryCollection categories = source.getCategoryCollection(); while (categories.next()) { final Category category = categories.getCategory(); category.addChild(this, categories.getSortKey()); category.save(); // XXX remove me } categories.close(); return true; } @Override protected void initialize() { super.initialize(); addObserver(s_instancesObserver); m_wasNew = isNew(); } @Override protected void afterSave() { if (m_wasNew) { getPrimaryInstance().setContentSection(getContentSection()); } super.afterSave(); } // Utility methods private DataAssociationCursor instances() { final DataAssociationCursor cursor = ((DataAssociation) super.get(INSTANCES)).cursor(); return cursor; } private DataAssociationCursor instances(final String version) { final DataAssociationCursor cursor = instances(); cursor.addEqualsFilter(VERSION, version); return cursor; } }