Index: hst/src/main/java/org/onehippo/forge/properties/api/PropertiesManager.java =================================================================== --- hst/src/main/java/org/onehippo/forge/properties/api/PropertiesManager.java (revision 155) +++ hst/src/main/java/org/onehippo/forge/properties/api/PropertiesManager.java (working copy) @@ -124,7 +124,21 @@ * Use PropertiesUtil to get a Map directly. */ Map getProperties(final String[] paths, final HippoBean baseBean); + + /** + * Returns a map with String name/value pairs. + * + * Get the properties from the document with the given name at the configured location, + * relative to the given base bean (if configured location does not start with slash). + * + * @param path the relative path of the properties documents to search for + * @param baseBean the base bean from where to get the properties location (if location does not start with slash), + * normally the siteContentBaseBean. + * @param locale the locale by which to try to retrieve linked translated properties. + */ + Map getProperties(final HippoBean baseBean, final String path, final Locale locale); + /** * Invalidate a cached document based on the canonical path of a properties document, * or invalidate all if the path is null. Index: hst/src/main/java/org/onehippo/forge/properties/impl/AbstractPropertiesManager.java =================================================================== --- hst/src/main/java/org/onehippo/forge/properties/impl/AbstractPropertiesManager.java (revision 155) +++ hst/src/main/java/org/onehippo/forge/properties/impl/AbstractPropertiesManager.java (working copy) @@ -76,4 +76,16 @@ return new PropertiesMap(beans); } + /** + * {@inheritDoc} + */ + @Override + public Map getProperties(HippoBean baseBean, String path, Locale locale) { + PropertiesBean propertiesBean = getPropertiesBean(path, baseBean, locale); + return (propertiesBean != null) ? new PropertiesMap(propertiesBean) : null; + } + + + + } Index: hst/src/main/java/org/onehippo/forge/properties/impl/CachingMapPropertiesManagerImpl.java =================================================================== --- hst/src/main/java/org/onehippo/forge/properties/impl/CachingMapPropertiesManagerImpl.java (revision 0) +++ hst/src/main/java/org/onehippo/forge/properties/impl/CachingMapPropertiesManagerImpl.java (working copy) @@ -0,0 +1,156 @@ +package org.onehippo.forge.properties.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.commons.lang.StringUtils; +import org.hippoecm.hst.content.beans.standard.HippoBean; +import org.hippoecm.repository.api.HippoNode; +import org.onehippo.forge.properties.annotated.Properties; +import org.onehippo.forge.properties.api.PropertiesManager; +import org.onehippo.forge.properties.api.PropertiesUtil; +import org.onehippo.forge.properties.bean.PropertiesBean; + +public class CachingMapPropertiesManagerImpl extends PropertiesManagerImpl implements PropertiesManager{ + + private final static Map> beansCache = Collections.synchronizedMap(new HashMap>()); + + // Cache of locale specific keys: key is properties document canonical path (without locale). + // Reason is that invalidation occurs without locale because it's JCR event based (no locale available) + private final static Map> localeVariantKeysCache = Collections.synchronizedMap(new HashMap>()); + + @Override + public void invalidate(final String canonicalPath) { + if (canonicalPath != null) { + List localeVariants = localeVariantKeysCache.remove(canonicalPath); + if (localeVariants != null) { + for (String localeVariantPath : localeVariants) { + beansCache.remove(localeVariantPath); + } + } + } + else { + // according to interface, invalidate all if argument is null + localeVariantKeysCache.clear(); + beansCache.clear(); + } + } + + public Map getPropertiesMapByLocale(final HippoBean location, final String path, final Locale locale) { + + if (location == null) { + throw new IllegalArgumentException("Location bean is null"); + } + if (path == null) { + throw new IllegalArgumentException("Path is null"); + } + + try { + final String canonicalKey = createCanonicalKey(location, path); + String localeKey = canonicalKey; + if (locale != null) { + localeKey += "/" + locale.toString(); + } + + // check if cached + Map propertiesMap = getFromCache(localeKey); + if (propertiesMap != null) { + return propertiesMap; + } + + // create and cache bean + final Properties propertiesDoc = getTranslatedProperties(location, path, locale); + if (propertiesDoc != null) { + propertiesMap = PropertiesUtil.toMap(new PropertiesBean(propertiesDoc)); + + storeInCache(canonicalKey, localeKey, propertiesMap); + + return propertiesMap; + } + } catch (RepositoryException e) { + throw new IllegalStateException(e); + } + + return null; + } + + /** + * Get a properties bean from cache. + */ + protected Map getFromCache(final String localeKey) { + return beansCache.get(localeKey); + } + + /** + * Store a properties bean in cache. + * + * NB synchronized method: although the caches are synchronized themselves, adding values to cached lists is not. + */ + protected synchronized void storeInCache(final String canonicalKey, final String localeKey, final Map propertiesBean) { + + // Keep track of the locale variants in second cache. + // Reason is that invalidation occurs without locale because it's JCR event based (no locale available, just path) + final List localeVariantKeys = localeVariantKeysCache.get(canonicalKey); + if (localeVariantKeys == null) { + final List values = new ArrayList(1); + values.add(localeKey); + localeVariantKeysCache.put(canonicalKey, values); + } + else if (!localeVariantKeys.contains(localeKey)) { + localeVariantKeys.add(localeKey); + } + + // put bean in actual cache + beansCache.put(localeKey, propertiesBean); + } + + /** + * Create a key for a location an a relative path of a properties document. + */ + protected String createCanonicalKey(final HippoBean location, final String path) throws RepositoryException { + + // path contains folder(s): construct a canonical path with folder(s)/handle/document + + // construct a canonical path with (folders)/handle/document so duplicate the last part of the path + final String docName = (path.lastIndexOf("/") < 0) ? path : path.substring(path.lastIndexOf("/") + 1); + StringBuilder sb = new StringBuilder(((HippoNode) location.getNode()).getCanonicalNode().getPath()); + sb.append('/'); + sb.append(path); + sb.append('/'); + sb.append(docName); + return sb.toString(); + } + + + + @Override + public Map getProperties(HippoBean baseBean) { + return getProperties(baseBean, getDefaultDocumentName(), (Locale) null); + + } + + @Override + public Map getProperties(String[] paths, HippoBean baseBean) { + Map result = new HashMap(); + for (String path : paths) { + getProperties(baseBean, path, (Locale) null); + } + return result; + } + + + @Override + public Map getProperties(HippoBean baseBean, String path, Locale locale) { + if (StringUtils.isBlank(path)) { + path = getDefaultDocumentName(); + } + return getPropertiesMapByLocale(getDefaultLocation(baseBean), path, locale); + } + +} Index: hst/src/main/java/org/onehippo/forge/properties/tags/PropertyTag.java =================================================================== --- hst/src/main/java/org/onehippo/forge/properties/tags/PropertyTag.java (revision 155) +++ hst/src/main/java/org/onehippo/forge/properties/tags/PropertyTag.java (working copy) @@ -71,10 +71,9 @@ // use PropertiesManager API to retrieve property map final HippoBean siteContentBaseBean = this.getSiteContentBaseBean(hstRequest); - final PropertiesBean propertiesBean = (language == null) ? - propertiesManager.getPropertiesBean(documentPath, siteContentBaseBean, hstRequest.getLocale()) : - propertiesManager.getPropertiesBean(documentPath, siteContentBaseBean, LocaleUtils.toLocale(language)); - final Map properties = PropertiesUtil.toMap(propertiesBean); + final Map properties = (language == null) ? + propertiesManager.getProperties(siteContentBaseBean, documentPath, hstRequest.getLocale()) : + propertiesManager.getProperties(siteContentBaseBean, documentPath, LocaleUtils.toLocale(language)); if (properties == null) { handleValue(getDefaultValue(propertiesManager), hstRequest);