/* * 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.dispatcher; import com.arsdigita.bebop.PageState; import com.arsdigita.cms.CMS; import com.arsdigita.cms.ContentCenter; import com.arsdigita.util.Assert; import org.apache.log4j.Logger; import org.libreccm.categorization.Category; import org.librecms.contentsection.ContentItem; import org.librecms.contentsection.ContentSection; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.StringTokenizer; /** * Resolves items to URLs and URLs to items for multiple language * variants. * * Created Mon Jan 20 14:30:03 2003. * * @author Michael Hanisch * @version $Id: MultilingualItemResolver.java 2090 2010-04-17 08:04:14Z pboy $ */ public class MultilingualItemResolver extends AbstractItemResolver implements ItemResolver { private static final Logger s_log = Logger.getLogger (MultilingualItemResolver.class); private static MasterPage s_masterP = null; private static final String ADMIN_PREFIX = "admin"; /** * The string identifying an item's ID in the query string of a * URL. */ protected static final String ITEM_ID = "item_id"; /** * The separator used in URL query strings; should be either "&" * or ";". */ protected static final String SEPARATOR = "&"; public MultilingualItemResolver() { s_log.debug("Undergoing creation"); } /** * Returns a content item based on section, url, and use context. * * @param section The current content section * @param url The section-relative URL * @param context The use context, * e.g. ContentItem.LIVE, * CMSDispatcher.PREVIEW or * ContentItem.DRAFT. See {@link * #getCurrentContext}. * @return The content item, or null if no such item exists */ @Override public ContentItem getItem(final ContentSection section, String url, final String context) { if (s_log.isDebugEnabled()) { s_log.debug("Resolving the item in content section " + section + " at URL '" + url + "' for context " + context); } Assert.exists(section, "ContentSection section"); Assert.exists(url, "String url"); Assert.exists(context, "String context"); Category rootFolder = section.getRootDocumentsFolder(); url = stripTemplateFromURL(url); // nothing to do, if root folder is null if (rootFolder == null) { s_log.debug("The root folder is null; returning no item"); } else { if (s_log.isDebugEnabled()) { s_log.debug("Using root folder " + rootFolder); } if ("live".equals(context)) { s_log.debug("The use context is 'live'"); // We allow for returning null, so the root folder may // not be live. //Assert.isTrue(rootFolder.isLive(), // "live context - root folder of secion must be live"); // If the context is 'live', we need the live item. rootFolder = (Folder) rootFolder.getLiveVersion(); if (rootFolder == null) { s_log.debug("The live version of the root folder is " + "null; returning no item"); } else { s_log.debug("The root folder has a live version; " + "recursing"); final String prefix = section.getURL() + rootFolder.getPath(); if (url.startsWith(prefix)) { if (s_log.isDebugEnabled()) { s_log.debug("The URL starts with prefix '" + prefix + "'; removing it"); } url = url.substring(prefix.length()); } final ContentItem item = getItemFromLiveURL(url, rootFolder); if (s_log.isDebugEnabled()) { s_log.debug("Resolved URL '" + url + "' to item " + item); } return item; } } else if (ContentItem.DRAFT.equals(context)) { s_log.debug("The use context is 'draft'"); // For 'draft' items, 'generateUrl()' method returns // URL like this // '/acs/wcms/admin/item.jsp?item_id=10201&set_tab=1' // Check if URL contains any match of string // 'item_id', then try to instanciate item_id value // and return FIXME: Please hack this if there is // more graceful solution. [aavetyan] if (Assert.isEnabled()) { Assert.isTrue (url.indexOf(ITEM_ID) >= 0, "url must contain parameter " + ITEM_ID); } final ContentItem item = getItemFromDraftURL(url); if (s_log.isDebugEnabled()) { s_log.debug("Resolved URL '" + url + "' to item " + item); } return item; } else if (CMSDispatcher.PREVIEW.equals(context)) { s_log.debug("The use context is 'preview'"); final String prefix = CMSDispatcher.PREVIEW + "/"; if (url.startsWith(prefix)) { if (s_log.isDebugEnabled()) { s_log.debug("The URL starts with prefix '" + prefix + "'; removing it"); } url = url.substring(prefix.length()); } final ContentItem item = getItemFromLiveURL(url, rootFolder); if (s_log.isDebugEnabled()) { s_log.debug("Resolved URL '" + url + "' to item " + item); } return item; } else { throw new IllegalArgumentException ("Invalid item resolver context " + context); } } s_log.debug("No item resolved; returning null"); return null; } /** * Fetches the current context based on the page state. * * @param state the current page state * @return the context of the current URL, such as * ContentItem.LIVE or ContentItem.DRAFT * @see ContentItem#LIVE * @see ContentItem#DRAFT */ @Override public String getCurrentContext(final PageState state) { s_log.debug("Getting the current context"); // XXX need to use Web.getWebContext().getRequestURL() here. String url = state.getRequest().getRequestURI(); final ContentSection section = CMS.getContext().getContentSection(); // If this page is associated with a content section, // transform the URL so that it is relative to the content // section site node. if (section != null) { final String sectionURL = section.getURL(); if (url.startsWith(sectionURL)) { url = url.substring(sectionURL.length()); } } // Remove any template-specific URL components (will only work // if they're first in the URL at this point; verify). XXX but // we don't actually verify? url = stripTemplateFromURL(url); // Determine if we are under the admin UI. if (url.startsWith(ADMIN_PREFIX) || url.startsWith(ContentCenter.getURL())) { return ContentItem.DRAFT; } else { return ContentItem.LIVE; } } /** * Generates a URL for a content item. * * @param itemId The item ID * @param name The name of the content page * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or * "admin" * @return The URL of the item * @see #getCurrentContext */ @Override public String generateItemURL(final PageState state, final BigDecimal itemId, final String name, final ContentSection section, final String context) { return generateItemURL(state, itemId, name, section, context, null); } /** * Generates a URL for a content item. * * @param itemId The item ID * @param name The name of the content page * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or * "admin" * @param templateContext the context for the URL, such as * "public" * @return The URL of the item * @see #getCurrentContext */ @Override public String generateItemURL(final PageState state, final BigDecimal itemId, final String name, final ContentSection section, final String context, final String templateContext) { if (s_log.isDebugEnabled()) { s_log.debug("Generating an item URL for item id " + itemId + ", section " + section + ", and context '" + context + "' with name '" + name + "'"); } Assert.exists(itemId, "BigDecimal itemId"); Assert.exists(context, "String context"); Assert.exists(section, "ContentSection section"); if (ContentItem.DRAFT.equals(context)) { // No template context here. return generateDraftURL(section, itemId); } else if (CMSDispatcher.PREVIEW.equals(context)) { ContentItem item = new ContentItem(itemId); return generatePreviewURL(section, item, templateContext); } else if (ContentItem.LIVE.equals(context)) { ContentItem item = new ContentItem(itemId); if (Assert.isEnabled()) { Assert.exists(item, "item"); Assert.isTrue(ContentItem.LIVE.equals(item.getVersion()), "Generating " + ContentItem.LIVE + " " + "URL; this item must be the live version"); } return generateLiveURL(section, item, templateContext); } else { throw new IllegalArgumentException ("Unknown context '" + context + "'"); } } /** * Generates a URL for a content item. * * @param item The item * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or * "admin" * @return The URL of the item * @see #getCurrentContext */ @Override public String generateItemURL(final PageState state, final ContentItem item, final ContentSection section, final String context) { return generateItemURL(state, item, section, context, null); } /** * Generates a URL for a content item. * * @param item The item * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or * "admin" * @param templateContext the context for the URL, such as * "public" * @return The URL of the item * @see #getCurrentContext */ @Override public String generateItemURL(final PageState state, final ContentItem item, ContentSection section, final String context, final String templateContext) { if (s_log.isDebugEnabled()) { s_log.debug("Generating an item URL for item " + item + ", section " + section + ", and context " + context); } Assert.exists(item, "ContentItem item"); Assert.exists(context, "String context"); if (section == null) { section = item.getContentSection(); } if (ContentItem.DRAFT.equals(context)) { if (Assert.isEnabled()) { Assert.isTrue(ContentItem.DRAFT.equals(item.getVersion()), "Generating " + ContentItem.DRAFT + " url: item must be draft version"); } return generateDraftURL(section, item.getID()); } else if (CMSDispatcher.PREVIEW.equals(context)) { return generatePreviewURL(section, item, templateContext); } else if (ContentItem.LIVE.equals(context)) { if (Assert.isEnabled()) { Assert.isTrue(ContentItem.LIVE.equals(item.getVersion()), "Generating " + ContentItem.LIVE + " url: item must be live version"); } return generateLiveURL(section, item, templateContext); } else { throw new RuntimeException("Unknown context " + context); } } /** * Returns a master page based on page state (and content * section). * * @param item The content item * @param request The HTTP request * @return The master page * @throws javax.servlet.ServletException */ @Override public CMSPage getMasterPage(final ContentItem item, final HttpServletRequest request) throws ServletException { if (s_log.isDebugEnabled()) { s_log.debug("Getting the master page for item " + item); } // taken from SimpleItemResolver if (s_masterP == null) { s_masterP = new MasterPage(); s_masterP.init(); } if (s_log.isDebugEnabled()) { s_log.debug("Returning master page " + s_masterP); } return s_masterP; } /** * Returns content item's draft version URL * * @param section The content section to which the item belongs * @param itemId The content item's ID * @return generated URL string */ protected String generateDraftURL(final ContentSection section, final BigDecimal itemId) { if (s_log.isDebugEnabled()) { s_log.debug("Generating draft URL for item ID " + itemId + " and section " + section); } if (Assert.isEnabled()) { Assert.isTrue(section != null && itemId != null, "get draft url: neither secion nor item " + "can be null"); } final String url = ContentItemPage.getItemURL (section.getPath() + "/", itemId, ContentItemPage.AUTHORING_TAB); if (s_log.isDebugEnabled()) { s_log.debug("Generated draft URL " + url); } return url; } /** * Generate a language-independent URL to the * item in the given section.

When a client * retrieves this URL, the URL is resolved to point to a specific * language instance of the item referenced here, i.e. this URL * will be resolved to a language-specific URL * internally. * * @param section the ContentSection that contains this item * @param item ContentItem for which a URL should be * constructed. * @param templateContext template context; will be ignored if null * * @return a language-independent URL to the * item in the given section, which will * be presented within the given templateContext */ protected String generateLiveURL(final ContentSection section, final ContentItem item, final String templateContext) { if (s_log.isDebugEnabled()) { s_log.debug("Generating live URL for item " + item + " in " + "section " + section); } /* * URL = URL of section + templateContext + path to the ContentBundle * which contains the item */ final StringBuffer url = new StringBuffer(400); //url.append(section.getURL()); url.append(section.getPath()).append("/"); /* * add template context, if one is given */ // This is breaking URL's...not sure why it's here. XXX // this should work with the appropriate logic. trying again. if (!(templateContext == null || templateContext.length() == 0)) { url.append(TEMPLATE_CONTEXT_PREFIX).append(templateContext).append("/"); } // Try to retrieve the bundle. final ContentItem bundle = (ContentItem) item.getParent(); /* * It would be nice if we had a ContentPage here, which * supports the getContentBundle() method, but unfortunately * the API sucks and there is no real distinction between mere * ContentItems and top-level items, so we have to use this * hack. TODO: add sanity check that bundle is actually a * ContentItem. */ if (bundle != null && bundle instanceof ContentBundle) { s_log.debug("Found a bundle; building its file name"); final String fname = bundle.getPath(); if (s_log.isDebugEnabled()) { s_log.debug("Appending the bundle's file name '" + fname + "'"); } url.append(fname); } else { s_log.debug("No bundle found; using the item's path directly"); url.append(item.getPath()); } /* * This will append the language to the url, which will in turn render * the language negotiation inoperative final String language = item.getLanguage(); if (language == null) { s_log.debug("The item has no language; omitting the " + "language encoding"); } else { s_log.debug("Encoding the language of the item passed in, '" + language + "'"); url.append("." + language); } if (s_log.isDebugEnabled()) { s_log.debug("Generated live URL " + url.toString()); } */ return url.toString(); } /** * Generate a URL which can be used to preview the * item, using the given * templateContext.

Only a specific language * instance can be previewed, meaning there no language * negotiation is involved when a request is made to a URL that * has been generated by this method. * * @param section The ContentSection which contains * the item * @param item The ContentItem for which a URL should * be generated. * @param templateContext the context that determines which * template should render the item when it is previewed; ignored * if the argument given here is null * @return a URL which can be used to preview the given * item */ protected String generatePreviewURL(ContentSection section, ContentItem item, String templateContext) { Assert.exists(section, "ContentSection section"); Assert.exists(item, "ContentItem item"); final StringBuffer url = new StringBuffer(100); url.append(section.getPath()); url.append("/"); url.append(CMSDispatcher.PREVIEW); url.append("/"); /* * add template context, if one is given */ // This is breaking URL's...not sure why it's here. XXX // this should work with the appropriate logic. trying again. if (!(templateContext == null || templateContext.length() == 0)) { url.append(TEMPLATE_CONTEXT_PREFIX).append(templateContext).append("/"); } // Try to retrieve the bundle. ContentItem bundle = (ContentItem) item.getParent(); /* It would be nice if we had a ContentPage here, which * supports the getContentBundle() method, but unfortunately * the API sucks and there is no real distinction between mere * ContentItems and top-level items, so we have to use this * hack. TODO: add sanity check that bundle is actually a * ContentItem. */ if (bundle != null && bundle instanceof ContentBundle) { s_log.debug("Found a bundle; using its path"); url.append(bundle.getPath()); } else { s_log.debug("No bundle found; using the item's path directly"); url.append(item.getPath()); } final String language = item.getLanguage(); if (language == null) { s_log.debug("The item has no language; omitting the " + "language encoding"); } else { s_log.debug("Encoding the language of the item passed in, '" + language + "'"); url.append(".").append(language); } if (s_log.isDebugEnabled()) { s_log.debug("Generated preview URL " + url.toString()); } return url.toString(); } /** * Retrieves ITEM_ID parameter from URL and * instantiates item according to this ID. * * @param url URL that indicates which item should be retrieved; * must contain the ITEM_ID parameter * @return the ContentItem the given url * points to, or null if no ID has been found in the * url */ protected ContentItem getItemFromDraftURL(final String url) { if (s_log.isDebugEnabled()) { s_log.debug("Looking up the item from draft URL " + url); } int pos = url.indexOf(ITEM_ID); // XXX this is wrong: here we abort on not finding the // parameter; below we return null. if (Assert.isEnabled()) { Assert.isTrue(pos >= 0, "Draft URL must contain parameter " + ITEM_ID); } String item_id = url.substring(pos); // item_id == ITEM_ID=.... ? pos = item_id.indexOf("="); // should be exactly after the ITEM_ID string if (pos != ITEM_ID.length()) { // item_id seems to be something like ITEM_IDFOO= s_log.debug("No suitable item_id parameter found; returning null"); return null; // no ID found } pos++; // skip the "=" // ID is the string between the equal (at pos) and the next separator int i = item_id.indexOf(SEPARATOR); item_id = item_id.substring(pos, Math.min(i, item_id.length() -1)); if (s_log.isDebugEnabled()) { s_log.debug("Looking up item using item ID " + item_id); } OID oid = new OID(ContentItem.BASE_DATA_OBJECT_TYPE, new BigDecimal(item_id)); final ContentItem item = (ContentItem) DomainObjectFactory.newInstance (oid); if (s_log.isDebugEnabled()) { s_log.debug("Returning item " + item); } return item; } /** * Returns a content item based on URL relative to the root * folder. * * @param url The content item url * @param parentFolder The parent folder object, url must be relevant to it * @return The Content Item instance, it can also be either Bundle * or Folder objects, depending on URL and file language extension */ protected ContentItem getItemFromLiveURL(String url, Folder parentFolder) { if (s_log.isDebugEnabled()) { s_log.debug("Resolving the item for live URL " + url + " and parent folder " + parentFolder); } if (parentFolder == null || url == null || url.equals("")) { if (s_log.isDebugEnabled()) { s_log.debug("The url is null or parent folder was null " + "or something else is wrong, so just return " + "the parent folder"); } return parentFolder; } int len = url.length(); int index = url.indexOf('/'); if (index >= 0) { s_log.debug("The URL starts with a slash; paring off the first " + "URL element and recursing"); // If we got first slash (index == 0), ignore it and go // on, sample '/foo/bar/item.html.en', in next recursion // will have deal with 'foo' folder. String name = index > 0 ? url.substring(0, index) : ""; parentFolder = "".equals(name) ? parentFolder : (Folder) parentFolder.getItem(URLEncoder.encode(name), true); url = index + 1 < len ? url.substring(index + 1) : ""; return getItemFromLiveURL(url, parentFolder); } else { s_log.debug("Found a file element in the URL"); String[] nameAndLang = getNameAndLangFromURLFrag(url); String name = nameAndLang[0]; String lang = nameAndLang[1]; ContentItem item = parentFolder.getItem(URLEncoder.encode(name), false); return getItemFromLangAndBundle(lang, item); } } /** * Returns an array containing the the item's name and lang based * on the URL fragment. * * @return a two-element string array, the first element * containing the bundle name, and the second element containing * the lang string */ protected String[] getNameAndLangFromURLFrag(String url) { String name = null; String lang = null; /* * Try to find out if there's an extension with the language code * 1 Get a list of all "extensions", i.e. parts of the url * which are separated by colons * 2 If one or more extensions have been found, compare them against * the list of known language codes * 2a if a match is found, this language is used to retrieve an instance * from a bundle * 2b if no match is found */ final ArrayList exts = new ArrayList(5); final StringTokenizer tok = new StringTokenizer(url, "."); while (tok.hasMoreTokens()) { exts.add(tok.nextToken()); } if (exts.size() > 0) { if (s_log.isDebugEnabled()) { s_log.debug("Found some file extensions to look at; they " + "are " + exts); } /* * We have found at least one extension, so we can * proceed. Now try to find out if one of the * extensions looks like a language code (we only * support 2-letter language codes!). * If so, use this as the language to look for. */ /* * First element is the NAME of the item, not an extension! */ name = (String) exts.get(0); String ext = null; Collection supportedLangs = LanguageUtil.getSupportedLanguages2LA(); Iterator supportedLangIt = null; for (int i = 1; i < exts.size(); i++) { ext = (String) exts.get(i); if (s_log.isDebugEnabled()) { s_log.debug("Examining extension " + ext); } /* * Loop through all extensions, but discard the * first one, which is the name of the item. */ if (ext != null && ext.length() == 2) { /* Only check extensions consisting of 2 * characters. * * Compare current extension with known * languages; if it matches, we've found the * language we should use! */ supportedLangIt = supportedLangs.iterator(); while (supportedLangIt.hasNext()) { if (ext.equals(supportedLangIt.next())) { lang = ext; if (s_log.isDebugEnabled()) { s_log.debug("Found a match; using " + "language " + lang); } break; } } } else { if (s_log.isDebugEnabled()) { s_log.debug("Discarding extension " + ext + "; " + "it is too short"); } } } } else { s_log.debug("The file has no extensions; no language was " + "encoded"); name = url; // no extension, so we just have a name here lang = null; // no extension, so we cannot guess the language } if (Assert.isEnabled()) { Assert.exists(name, "String name"); Assert.exists(lang == null || lang.length() == 2); } if (s_log.isDebugEnabled()) { s_log.debug("File name resolved to " + name); s_log.debug("File language resolved to " + lang); } String[] returnArray = new String[2]; returnArray[0] = name; returnArray[1] = lang; return returnArray; } /** * Finds a language instance of a content item given the bundle, * name, and lang string * * @param lang The lang string from the URL * @param item The content bundle * * @return The negotiated lang instance for the current request. */ protected ContentItem getItemFromLangAndBundle(String lang, ContentItem item) { if (item != null && item instanceof ContentBundle) { if (s_log.isDebugEnabled()) { s_log.debug("Found content bundle " + item); } if (lang == null) { s_log.debug("The URL has no language encoded in it; " + "negotiating the language"); // There is no language, so we get the negotiated locale and call // this method again with a proper language return this.getItemFromLangAndBundle(GlobalizationHelper.getNegotiatedLocale().getLanguage(), item); } else { s_log.debug("The URL is encoded with a langauge; " + "fetching the appropriate item from " + "the bundle"); /* * So the request contains a language code as an * extension of the "name" ==>go ahead and try to * find the item from its ContentBundle. Fail if * the bundle does not contain an instance for the * given language. */ final ContentItem resolved = ((ContentBundle) item).getInstance(lang); if (s_log.isDebugEnabled()) { s_log.debug("Resolved URL to item " + resolved); } return resolved; } } else { if (s_log.isDebugEnabled()) { s_log.debug("I expected to get a content bundle; I got " + item); } /* * We expected something like a Bundle, but it seems * like we got something completely different... just * return this crap and let other people's code deal * with it ;-). * * NOTE: This should never happen :-) */ return item; // might be null } } }