/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * MetadataManager.java * Creation date: Jul 9, 2003 * By: Frank Worsley */ package org.openquark.cal.metadata; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.openquark.cal.compiler.ClassInstance; import org.openquark.cal.compiler.ModuleName; import org.openquark.cal.compiler.ScopedEntity; import org.openquark.cal.module.Cal.Collections.CAL_List; import org.openquark.cal.module.Cal.Core.CAL_Prelude; import org.openquark.cal.services.CALFeatureName; import org.openquark.cal.services.LocaleUtilities; import org.openquark.cal.services.LocalizedResourceName; import org.openquark.cal.services.MetaModule; import org.openquark.cal.services.ModuleResourceManager; import org.openquark.cal.services.ResourceName; import org.openquark.cal.services.ResourceStore; import org.openquark.cal.services.Status; import org.openquark.cal.services.WorkspaceResource; import org.openquark.cal.services.FeatureName.FeatureType; /** * This class implements the metadata manager responsible for saving and * loading CALFeatureMetadata objects to a MetadataStore using XML files. * * @author Frank Worsley */ public class MetadataManager extends ModuleResourceManager { /** The namespace for log messages from this package. */ public static final String METADATA_LOGGER_NAMESPACE = "org.openquark.cal.metadata"; /** An instance of a Logger for log messages from this package. */ static final Logger METADATA_LOGGER = Logger.getLogger(METADATA_LOGGER_NAMESPACE); /** The metadata cache for already loaded metadata. */ private final MetadataCache metadataCache = new MetadataCache(); /** * The lock for access to the metadata cache. * Using a zero-length byte[] instead of a new Object() is a small optimization. */ private final byte[] cacheLock = new byte[0]; /** The store for metadata for this manager. */ private final MetadataStore metadataStore; /** * Constructor for a metadata manager. * @param metadataStore the store for metadata managed by this manager. */ public MetadataManager(MetadataStore metadataStore) { this.metadataStore = metadataStore; } /** * @param entity the entity to check for * @param locale the locale to check. * @return true if metadata exists for the given entity, false otherwise */ public boolean hasMetadata(ScopedEntity entity, Locale locale) { return hasMetadata(CALFeatureName.getScopedEntityFeatureName(entity), locale); } /** * @param module the module to check for * @param locale the locale to check. * @return true if metadata exists for the given module, false otherwise */ public boolean hasMetadata(MetaModule module, Locale locale) { return hasMetadata(CALFeatureName.getModuleFeatureName(module), locale); } /** * @param instance the instance to check for * @param locale the locale to check. * @return true if metadata exists for the given instance, false otherwise */ public boolean hasMetadata(ClassInstance instance, Locale locale) { return hasMetadata(CALFeatureName.getClassInstanceFeatureName(instance), locale); } /** * @param featureName the feature to check * @param locale the locale to check. * @return whether metadata exists for the given feature. */ public boolean hasMetadata(CALFeatureName featureName, Locale locale) { return metadataStore.hasFeature(new LocalizedResourceName(featureName, locale)); } /** * @param entity the entity to get metadata for * @param locale the locale to check. * @return the metadata for the entity. If the entity has no metadata, then default metadata is returned. */ public ScopedEntityMetadata getMetadata(ScopedEntity entity, Locale locale) { return (ScopedEntityMetadata) getMetadata(CALFeatureName.getScopedEntityFeatureName(entity), locale); } /** * @param module the Module to get metadata for * @param locale the locale to check. * @return the metadata for the module. If the module has no metadata, then default metadata is returned. */ public ModuleMetadata getMetadata(MetaModule module, Locale locale) { return (ModuleMetadata) getMetadata(CALFeatureName.getModuleFeatureName(module), locale); } /** * @param instance the ClassInstance to get metadata for * @param locale the locale to check. * @return the metadata for the class instance. If the instance has no metadata, then default metadata is returned. */ public ClassInstanceMetadata getMetadata(ClassInstance instance, Locale locale) { return (ClassInstanceMetadata) getMetadata(CALFeatureName.getClassInstanceFeatureName(instance), locale); } /** * Fetches the metadata for the given feature name and the given locale. * * @param featureName the name of the feature to get metadata for * @param locale the locale to check. * @return the metadata for the feature. If the feature has no metadata, then default * metadata is returned. */ public CALFeatureMetadata getMetadata(CALFeatureName featureName, Locale locale) { LocalizedResourceName resourceName = new LocalizedResourceName(featureName, locale); InputStream metadataInputStream; Status loadStatus = null; CALFeatureMetadata metadata; // Keep track of the originally requested locale Locale originalLocale = locale; synchronized (cacheLock) { // Check if we already have this cached MetadataCacheEntry cacheEntry = metadataCache.get(resourceName); if (cacheEntry != null) { return cacheEntry.getMetadata(); } // Get the input stream for the metadata, and use this to load. metadataInputStream = metadataStore.getInputStream(resourceName); // go through the fallback mechanism if necessary // variant-specific locale not found, fallback to country-specific locale if (metadataInputStream == null && locale.getVariant().length() > 0) { locale = new Locale(locale.getLanguage(), locale.getCountry()); resourceName = new LocalizedResourceName(featureName, locale); metadataInputStream = metadataStore.getInputStream(resourceName); } // country-specific locale not found, fallback to language-specific locale if (metadataInputStream == null && locale.getCountry().length() > 0) { locale = new Locale(locale.getLanguage()); resourceName = new LocalizedResourceName(featureName, locale); metadataInputStream = metadataStore.getInputStream(resourceName); } // language-specific locale not found, fallback to neutral locale if (metadataInputStream == null) { locale = LocaleUtilities.INVARIANT_LOCALE; resourceName = new LocalizedResourceName(featureName, locale); metadataInputStream = metadataStore.getInputStream(resourceName); } if (metadataInputStream == null) { metadata = getEmptyMetadata(featureName, originalLocale); } else { loadStatus = new Status("Load status"); // note that we pass in the original locale unaffected // by the fallback mechanism - this is done on purpose: once the // fallback mechanism finds the proper resource to load, its content // is treated as though it is the metadata for the originally requested locale. metadata = MetadataPersistenceHelper.loadMetadata(featureName, originalLocale, metadataInputStream, loadStatus); } // Add to the cache. cacheEntry = new MetadataCacheEntry(metadata); metadataCache.put(cacheEntry); } if (loadStatus != null && loadStatus.getSeverity().compareTo(Status.Severity.WARNING) >= 0) { METADATA_LOGGER.log(Level.WARNING, "Problems were encountered loading metadata for " + featureName); METADATA_LOGGER.log(Level.WARNING, loadStatus.getDebugMessage()); } // Make sure we close any open stream. if (metadataInputStream != null) { try { metadataInputStream.close(); } catch (IOException ioe) { } } return metadata; } /** * Returns a List of ResourceNames for the metadata resources associated with the given feature name, across all locales. * @param featureName the name of the feature whose metadata resources across all locales are to be enumerated. * @return a List of ResourceNames, one for each localized metadata resource associated with the given feature name. */ public List<ResourceName> getMetadataResourceNamesForAllLocales(CALFeatureName featureName) { return metadataStore.getMetadataResourceNamesForAllLocales(featureName); } /** * Saves the given metadata object to permanent storage. * @param metadata the metadata object to save * @param saveStatus the tracking status object * @return true if metadata was saved, false otherwise */ public boolean saveMetadata(CALFeatureMetadata metadata, Status saveStatus) { return saveMetadata(metadata, metadata.getFeatureName(), metadata.getLocale(), saveStatus); } /** * Save the given metadata object to permanent storage using the given name. * @param metadata * @param metadataName * @param locale the locale of the metadata. * @param saveStatus * @return true if metadata was saved, false otherwise */ boolean saveMetadata(CALFeatureMetadata metadata, CALFeatureName metadataName, Locale locale, Status saveStatus) { LocalizedResourceName resourceName = new LocalizedResourceName(metadataName, locale); // Create an output stream for the metadata. OutputStream metadataOutputStream = metadataStore.getOutputStream(resourceName, saveStatus); if (metadataOutputStream == null) { return false; } synchronized (cacheLock) { // Save the metadata MetadataPersistenceHelper.saveMetadata(metadata, metadataOutputStream); // Save the metadata in the cache. MetadataCacheEntry cacheEntry = new MetadataCacheEntry(metadata); metadataCache.put(cacheEntry); } try { // Make sure we flush and close any open stream. metadataOutputStream.flush(); metadataOutputStream.close(); } catch (IOException e) { METADATA_LOGGER.log(Level.WARNING, "A problem was encountered while saving metadata.", e); // return false; ?? } return true; } /** * @param entity the entity for which to get a metadata object * @param locale the locale associated with the empty metadata. * @return the correct instance of a CALFeatureMetadata object without any fields initialized */ public static ScopedEntityMetadata getEmptyMetadata(ScopedEntity entity, Locale locale) { return (ScopedEntityMetadata)getEmptyMetadata(CALFeatureName.getScopedEntityFeatureName(entity), locale); } /** * @param module the module for which to get a metadata object * @param locale the locale associated with the empty metadata. * @return the correct instance of a CALFeatureMetadata object without any fields initialized */ public static ModuleMetadata getEmptyMetadata(MetaModule module, Locale locale) { return (ModuleMetadata) getEmptyMetadata(CALFeatureName.getModuleFeatureName(module), locale); } /** * @param instance the class instanceo for which to get a metadata object * @param locale the locale associated with the empty metadata. * @return the correct instance of a CALFeatureMetadata object without any fields initialized */ public static ClassInstanceMetadata getEmptyMetadata(ClassInstance instance, Locale locale) { return (ClassInstanceMetadata) getEmptyMetadata(CALFeatureName.getClassInstanceFeatureName(instance), locale); } /** * @param featureName the name of the feature to get empty metadata for * @param locale the locale associated with the empty metadata. * @return the correct instance of CALFeatureMetadata without any fields initialized */ public static CALFeatureMetadata getEmptyMetadata(CALFeatureName featureName, Locale locale) { FeatureType type = featureName.getType(); if (type == CALFeatureName.FUNCTION) { return new FunctionMetadata(featureName, locale); } else if (type == CALFeatureName.CLASS_METHOD) { return new ClassMethodMetadata(featureName, locale); } else if (type == CALFeatureName.DATA_CONSTRUCTOR) { return new DataConstructorMetadata(featureName, locale); } else if (type == CALFeatureName.TYPE_CLASS) { return new TypeClassMetadata(featureName, locale); } else if (type == CALFeatureName.TYPE_CONSTRUCTOR) { return new TypeConstructorMetadata(featureName, locale); } else if (type == CALFeatureName.MODULE) { return new ModuleMetadata(featureName, locale); } else if (type == CALFeatureName.CLASS_INSTANCE) { return new ClassInstanceMetadata(featureName, locale); } else if (type == CALFeatureName.INSTANCE_METHOD) { return new InstanceMethodMetadata(featureName, locale); } throw new IllegalArgumentException("feature type not supported: " + type); } /** * {@inheritDoc} */ @Override protected boolean saveResource(ResourceName resourceName, InputStream sourceInputStream, Status saveStatus) { synchronized (cacheLock) { boolean result = super.saveResource(resourceName, sourceInputStream, saveStatus); metadataCache.removeResource(resourceName); return result; } } /** * {@inheritDoc} */ @Override public void removeResource(ResourceName resourceName, Status removeStatus) { synchronized (cacheLock) { super.removeResource(resourceName, removeStatus); metadataCache.removeResource(resourceName); } } /** * {@inheritDoc} */ @Override public void removeModuleResources(ModuleName moduleName, Status removeStatus) { synchronized (cacheLock) { super.removeModuleResources(moduleName, removeStatus); metadataCache.removeModule(moduleName); } } /** * Remove all metadata from the manager. * @param removeStatus the tracking status object. */ @Override public void removeAllResources(Status removeStatus) { synchronized (cacheLock) { super.removeAllResources(removeStatus); metadataCache.clear(); } } /** * {@inheritDoc} */ @Override public String getResourceType() { return WorkspaceResource.METADATA_RESOURCE_TYPE; } /** * {@inheritDoc} */ @Override public ResourceStore getResourceStore() { return metadataStore; } } /** * This class implements a cache for already loaded metadata. It stores the * metadata in a HashMap using the metadata file as the key. * There is a maximum size for the cache and the least recently used data * is removed from the cache if it grows past its maximum size. * * @author Frank Worsley */ class MetadataCache { /** The maximum number of items to keep in the cache. */ private static final int MAXIMUM_CACHE_SIZE = 500; /** * The Map that stores the actual cache entries. * Modified so that the number of entries does not exceed MAXIMUM_CACHE_SIZE. **/ private final Map<LocalizedResourceName, MetadataCacheEntry> dataCache = new LinkedHashMap<LocalizedResourceName, MetadataCacheEntry>(MAXIMUM_CACHE_SIZE / 4, 0.75f, true) { private static final long serialVersionUID = 2316021820792831141L; @Override protected boolean removeEldestEntry(Entry<LocalizedResourceName, MetadataCacheEntry> eldest) { return size() > MAXIMUM_CACHE_SIZE; } }; /** * Adds a new entry to the cache. * @param cacheEntry the entry to add */ void put(MetadataCacheEntry cacheEntry) { dataCache.put (cacheEntry.getKey(), cacheEntry); } /** * @param resourceName the resource name for which to retrieve a cache entry * @return the MetadataCacheEntry associated with the given File, or null * if there is no such cache entry. */ MetadataCacheEntry get(LocalizedResourceName resourceName) { return dataCache.get(resourceName); } /** * Remove the named cache entry. * @param resourceName the name of the metadata resource to be removed. */ public void removeResource(ResourceName resourceName) { dataCache.remove(resourceName); } /** * Remove cache entries from the given module. * @param moduleName the name of the module. */ void removeModule(ModuleName moduleName) { // For now, there is nothing for it but to iterate over all the entries in the cache. for (Iterator<Map.Entry<LocalizedResourceName, MetadataCacheEntry>> it = dataCache.entrySet().iterator(); it.hasNext(); ) { Map.Entry<LocalizedResourceName, MetadataCacheEntry> mapEntry = it.next(); LocalizedResourceName cacheEntryKey = mapEntry.getKey(); CALFeatureName cacheEntryFeatureName = (CALFeatureName)cacheEntryKey.getFeatureName(); if (cacheEntryFeatureName.hasModuleName() && cacheEntryFeatureName.toModuleName().equals(moduleName)) { it.remove(); } } } /** * Clear the metadata cache. */ public void clear() { dataCache.clear(); } } /** * This class represents an entry in the metadata cache. * It encapsulates the GemMetadata and ArgumentMetadata associated with a cache entry. * @author Frank Worsley */ class MetadataCacheEntry { /** The metadata associated with this cache entry. */ private final CALFeatureMetadata metadata; /** * Constructor for a new MetadataCacheEntry. * @param metadata the metadata associated with this cache entry */ MetadataCacheEntry (CALFeatureMetadata metadata) { this.metadata = metadata.copy(); } /** * @return the name which acts as the key for this cache entry. */ LocalizedResourceName getKey() { if (metadata != null) { return metadata.getResourceName(); } return null; } /** * @return a copy of the cached metadata object */ CALFeatureMetadata getMetadata() { if (metadata != null) { return metadata.copy(); } return null; } } /** * This class tests concurrent access to the metadata cache. * To exercise this test, first set the size of the metadata cache to 1 or 2. * @author Edward Lam */ class ConcurrentMetadataAccessTest { /** * This thread runs in a loop, and in each iteration attempts to fetch metadata for a single feature * which should be known to have metadata. Concurrent access failure will result in a failure to * find the metadata, a message being logged, or some other program failure. * @author Edward Lam */ private static class MetadataGetThread extends Thread { private static final int nGets = 50000000; private final CALFeatureName featureNameToGet; private final MetadataManager metadataManager; /** * Constructor for a MetadataManager.MetadataGetThread. * @param metadataManager the metadata manager from which to get the metadata. * @param featureNameToGet the name of the feature for which to retrieve metadata from the manager. */ public MetadataGetThread(MetadataManager metadataManager, CALFeatureName featureNameToGet) { this.metadataManager = metadataManager; this.featureNameToGet = featureNameToGet; } /** * {@inheritDoc} */ @Override public void run() { Random random = new Random(); System.err.println("Looking for: " + featureNameToGet); for (int i = 0; i < nGets; i++) { CALFeatureMetadata metadata = metadataManager.getMetadata(featureNameToGet, null); if (metadata == null) { System.err.println("Metadata not found for: " + featureNameToGet); } // Sleep a random amount of time from 0 - 4 ms. try { sleep(random.nextInt(5)); } catch (InterruptedException e) { e.printStackTrace(); } } System.err.println("Finished looking for: " + featureNameToGet); } } /** * Test target. Creates six threads, attempting concurrent access metadata for three functions. * @param args */ public static void main(String[] args) { MetadataManager metadataManager = new MetadataManager(new MetadataNullaryStore()); MetadataGetThread thread1 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_Prelude.Functions.abs)); MetadataGetThread thread2 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_Prelude.Functions.signum)); MetadataGetThread thread3 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_List.Functions.any)); MetadataGetThread thread4 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_Prelude.Functions.abs)); MetadataGetThread thread5 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_Prelude.Functions.signum)); MetadataGetThread thread6 = new MetadataGetThread(metadataManager, CALFeatureName.getFunctionFeatureName(CAL_List.Functions.any)); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); thread6.start(); try { thread1.join(); thread2.join(); thread3.join(); thread4.join(); thread5.join(); thread6.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }