/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program 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 General Public License for more details. * * * Copyright 2006 - 2016 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.plugin.services.metadata; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.metadata.model.Domain; import org.pentaho.metadata.model.LogicalModel; import org.pentaho.metadata.model.concept.IConcept; import org.pentaho.metadata.repository.DomainAlreadyExistsException; import org.pentaho.metadata.repository.DomainIdNullException; import org.pentaho.metadata.repository.DomainStorageException; import org.pentaho.metadata.repository.IMetadataDomainRepository; import org.pentaho.metadata.util.LocalizationUtil; import org.pentaho.metadata.util.XmiParser; import org.pentaho.platform.api.engine.PentahoAccessControlException; import org.pentaho.platform.api.repository2.unified.IAclNodeHelper; import org.pentaho.platform.api.repository2.unified.IUnifiedRepository; import org.pentaho.platform.api.repository2.unified.RepositoryFile; import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl; import org.pentaho.platform.api.repository2.unified.RepositoryFilePermission; import org.pentaho.platform.api.repository2.unified.UnifiedRepositoryException; import org.pentaho.platform.api.repository2.unified.data.simple.SimpleRepositoryFileData; import org.pentaho.platform.plugin.services.messages.Messages; import org.pentaho.platform.repository2.unified.RepositoryUtils; import org.pentaho.platform.repository2.unified.fileio.RepositoryFileInputStream; import org.pentaho.platform.repository2.unified.jcr.JcrAclNodeHelper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Handles the storage and retrieval of Pentaho Metada Domain objects in a repository. It does this by using a * pre-defined system location (defined by {@link PentahoMetadataDomainRepositoryInfo}) as the storage location for the * Domain files and associated locale files. </p> Since Domain IDs are the unique identifier for Pentaho Metadata * domains and may contain any character (including repository folder separator character(s) like '/', a {@link UUID} * will be created to store each file. The metadata for the file will be used to store the information (such as the * Domain ID). </p> * * @author <a href="mailto:dkincade@pentaho.com">David M. Kincade</a> */ public class PentahoMetadataDomainRepository implements IMetadataDomainRepository, IModelAnnotationsAwareMetadataDomainRepositoryImporter, IAclAwarePentahoMetadataDomainRepositoryImporter, IPentahoMetadataDomainRepositoryExporter { // The logger for this class private static final Log logger = LogFactory.getLog( PentahoMetadataDomainRepository.class ); // The messages object used in generating messages that may be seen by the user private static final Messages messages = Messages.getInstance(); private static final Map<IUnifiedRepository, PentahoMetadataInformationMap> metaMapStore = new HashMap<IUnifiedRepository, PentahoMetadataInformationMap>(); // The type of repository file (domain, locale) private static final String PROPERTY_NAME_TYPE = "file-type"; private static final String TYPE_DOMAIN = "domain"; private static final String TYPE_LOCALE = "locale"; // The repository file metadata key used to store the file's domain id private static final String PROPERTY_NAME_DOMAIN_ID = "domain-id"; // The repository file metadata key used to store the file's locale (properties files) private static final String PROPERTY_NAME_LOCALE = "locale"; // The default encoding for file storage private static final String DEFAULT_ENCODING = "UTF-8"; // The default mime-type for the Pentaho Domain files private static final String DOMAIN_MIME_TYPE = "text/xml"; // The default mime-type for locale files private static final String LOCALE_MIME_TYPE = "text/plain"; // caching immutable object private static final EnumSet<RepositoryFilePermission> READ = EnumSet.of( RepositoryFilePermission.READ ); // The repository used to store / retrieve objects private IUnifiedRepository repository; // Mapping between the Pentaho Metadata Domain ID and the repository files for that Domain private final PentahoMetadataInformationMap metadataMapping; // The parser used to serialize / deserialize metadata files private XmiParser xmiParser; // The repository utility class private RepositoryUtils repositoryUtils; // The localization utility class (used to load side-car properties files into a Domain object) private LocalizationUtil localizationUtil; private IAclNodeHelper aclHelper; private final ReentrantReadWriteLock lock; private boolean needToReload; /** * Creates an instance of this class providing the {@link IUnifiedRepository} repository backend. * * @param repository the {@link IUnifiedRepository} in which data will be stored / retrieved */ public PentahoMetadataDomainRepository( final IUnifiedRepository repository ) { this( repository, null, null, null ); } /** * Helper constructor used for setting other objects in this class * * @param repository the {@link IUnifiedRepository} in which data will be stored / retrieved * @param repositoryUtils utility class for working inside the repository </br>(NOTE: {@code null} is acceptable and * will create a default instance) * @param xmiParser the parser class for serializing / de-serializing Domain objects </br>(NOTE: {@code null} * is acceptable and will create a default instance) * @param localizationUtil the object used to add locale bundles into a Pentaho Metadata Domain object </br>(NOTE: * {@code null} is acceptable and will create a default instance) */ protected PentahoMetadataDomainRepository( final IUnifiedRepository repository, final RepositoryUtils repositoryUtils, final XmiParser xmiParser, final LocalizationUtil localizationUtil ) { if ( null == repository ) { throw new IllegalArgumentException(); } this.metadataMapping = getMetadataMapping( repository ); setRepository( repository ); setRepositoryUtils( repositoryUtils ); setLocalizationUtil( localizationUtil ); setXmiParser( xmiParser ); this.lock = new ReentrantReadWriteLock(); this.needToReload = true; } /** * Store a domain to the repository. The domain should persist between JVM restarts. * * @param domain domain object to store * @param overwrite if true, overwrite existing domain * @throws DomainIdNullException if domain id is null or empty * @throws DomainAlreadyExistsException if a domain with the same Domain ID already exists in the repository and * {@code overwrite == false} * @throws DomainStorageException if there is a problem storing the domain */ @Override public void storeDomain( final Domain domain, final boolean overwrite ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { if ( logger.isDebugEnabled() ) { logger.debug( "storeDomain(domain(id=" + ( domain != null ? domain.getId() : "" ) + ", " + overwrite + ")" ); } if ( null == domain || StringUtils.isEmpty( domain.getId() ) ) { throw new DomainIdNullException( messages .getErrorString( "PentahoMetadataDomainRepository.ERROR_0001_DOMAIN_ID_NULL" ) ); } String xmi = ""; try { // NOTE - a ByteArrayInputStream doesn't need to be closed ... // ... so this is safe AS LONG AS we use a ByteArrayInputStream xmi = xmiParser.generateXmi( domain ); //final InputStream inputStream = new ByteArrayInputStream( xmi.getBytes( DEFAULT_ENCODING ) ); final InputStream inputStream = new ByteArrayInputStream( xmi.getBytes( "UTF8" ) ); storeDomain( inputStream, domain.getId(), overwrite ); } catch ( DomainStorageException dse ) { throw dse; } catch ( DomainAlreadyExistsException dae ) { throw dae; } catch ( Exception e ) { final String errorMessage = messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0003_ERROR_STORING_DOMAIN", domain.getId(), e .getLocalizedMessage() ); logger.error( errorMessage, e ); throw new DomainStorageException( xmi + errorMessage, e ); } } protected String endsWithXmi( String value ) { if ( value.endsWith( ".xmi" ) ) { return value; } else { return value + ".xmi"; } } protected String noXmi( String value ) { if ( value.endsWith( ".xmi" ) ) { return value.substring( 0, value.length() - 4 ); } else { return value; } } protected String replaceDomainId( StringBuilder sb, String domainId ) { int datasourceModelTagPosition = sb.indexOf( "datasourceModel" ); if ( datasourceModelTagPosition != -1 ) { String xmiDomainId = endsWithXmi( domainId ); String noXmiDomainId = StringEscapeUtils.escapeXml( noXmi( domainId ) ); String tag = "<CWM:Description body="; int startTagPosition = sb.indexOf( tag, datasourceModelTagPosition ); int startPosition = startTagPosition + tag.length() + 1; int endPosition = sb.indexOf( "\"", startPosition ); String oldDomainId = sb.substring( startPosition, endPosition ); sb.delete( startPosition, endPosition ); if ( oldDomainId.endsWith( ".xmi" ) ) { sb.insert( startPosition, xmiDomainId ); } else { sb.insert( startPosition, noXmiDomainId ); } return xmiDomainId; } else { return domainId; } } protected String getDomainIdFromXmi( StringBuilder sb ) { int datasourceModelTagPosition = sb.indexOf( "datasourceModel" ); if ( datasourceModelTagPosition != -1 ) { String tag = "<CWM:Description body="; int startTagPosition = sb.indexOf( tag, datasourceModelTagPosition ); if ( startTagPosition != -1 ) { int startPosition = startTagPosition + tag.length() + 1; int endPosition = sb.indexOf( "\"", startPosition ); if ( endPosition != -1 ) { return StringEscapeUtils.unescapeXml( sb.substring( startPosition, endPosition ) ); } } } return null; } protected boolean isDomainIdXmiEqualsOrNotPresent( String domainId, String domainIdXmi ) { return domainIdXmi == null || noXmi( domainIdXmi ).equals( noXmi( domainId ) ); } /** * Stores a domain to the repository directly as an Input Stream * * @param inputStream * @param domainId * @param overwrite */ @Override public void storeDomain( final InputStream inputStream, final String domainId, final boolean overwrite ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { storeDomain( inputStream, domainId, overwrite, null ); } @Override public void storeDomain( InputStream inputStream, String domainId, boolean overwrite, RepositoryFileAcl acl ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { if ( logger.isDebugEnabled() ) { logger.debug( String.format( "storeDomain(inputStream, %s, %s, %s)", domainId, overwrite, acl ) ); } if ( null == inputStream ) { throw new IllegalArgumentException(); } if ( StringUtils.isEmpty( domainId ) ) { throw new DomainIdNullException( messages .getErrorString( "PentahoMetadataDomainRepository.ERROR_0001_DOMAIN_ID_NULL" ) ); } // Check to see if the domain already exists final RepositoryFile domainFile = getMetadataRepositoryFile( domainId ); if ( !overwrite && domainFile != null ) { final String errorString = messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0002_DOMAIN_ALREADY_EXISTS", domainId ); logger.error( errorString ); throw new DomainAlreadyExistsException( errorString ); } // Check if this is valid xml InputStream inputStream2; String xmi; try { // try to see if the xmi can be parsed (ie, check if it's valid xmi) // first, convert our input stream to a string StringBuilder stringBuilder = new StringBuilder(); BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream, DEFAULT_ENCODING ) ); try { while ( ( xmi = reader.readLine() ) != null ) { stringBuilder.append( xmi ); } } finally { inputStream.close(); } if ( !isDomainIdXmiEqualsOrNotPresent( domainId, getDomainIdFromXmi( stringBuilder ) ) ) { domainId = replaceDomainId( stringBuilder, domainId ); } xmi = stringBuilder.toString(); // now, try to see if the xmi can be parsed (ie, check if it's valid xmi) byte[] xmiBytes = xmi.getBytes( DEFAULT_ENCODING ); inputStream2 = new java.io.ByteArrayInputStream( xmiBytes ); xmiParser.parseXmi( inputStream2 ); // xmi is valid. Create a new inputstream for the actual import action. inputStream2.reset(); } catch ( Exception ex ) { logger.error( ex.getMessage() ); // throw new // DomainStorageException(messages.getErrorString("PentahoMetadataDomainRepository.ERROR_0010_ERROR_PARSING_XMI"), // ex); java.io.ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ex.printStackTrace( new java.io.PrintStream( byteArrayOutputStream ) ); throw new DomainStorageException( byteArrayOutputStream.toString(), ex ); } final SimpleRepositoryFileData data = new SimpleRepositoryFileData( inputStream2, DEFAULT_ENCODING, DOMAIN_MIME_TYPE ); final RepositoryFile newDomainFile; if ( domainFile == null ) { newDomainFile = createUniqueFile( domainId, null, data ); } else { newDomainFile = repository.updateFile( domainFile, data, null ); } // This invalidates any caching flushDomains(); getAclHelper().setAclFor( newDomainFile, acl ); } protected synchronized IAclNodeHelper getAclHelper() { if ( aclHelper == null ) { aclHelper = new JcrAclNodeHelper( repository ); } return aclHelper; } @Override public void setAclFor( String domainId, RepositoryFileAcl acl ) { getAclHelper().setAclFor( getMetadataRepositoryFile( domainId ), acl ); } @Override public RepositoryFileAcl getAclFor( String domainId ) { return getAclHelper().getAclFor( getMetadataRepositoryFile( domainId ) ); } @Override public boolean hasAccessFor( String domainId ) { if ( logger.isDebugEnabled() ) { logger.debug( "Checking access for: " + domainId ); } return getAclHelper().canAccess( getMetadataRepositoryFile( domainId ), READ ); } /** * This method can be called to avoid useless obtaining of repository file if the file has already been loaded * * @param repositoryFile repository file * @return delegates the call to <code>getAclHelper().canAccess()</code> */ private boolean hasAccessFor( RepositoryFile repositoryFile ) { return getAclHelper().canAccess( repositoryFile, READ ); } /* * retrieves the data streams for the metadata referenced by domainId. This could be a single .xmi file or an .xmi * file and multiple .properties files. */ public Map<String, InputStream> getDomainFilesData( final String domainId ) { Set<RepositoryFile> metadataFiles; lock.readLock().lock(); try { metadataFiles = metadataMapping.getFiles( domainId ); } finally { lock.readLock().unlock(); } Map<String, InputStream> values = new HashMap<String, InputStream>( metadataFiles.size() ); for ( RepositoryFile repoFile : metadataFiles ) { RepositoryFileInputStream is; try { is = new RepositoryFileInputStream( repoFile ); } catch ( Exception e ) { return null; // This pretty much ensures an exception will be thrown later and passed to the client } String fileName = repoFile.getName().endsWith( ".properties" ) ? repoFile.getName() : domainId + ( domainId.endsWith( ".xmi" ) ? "" : ".xmi" ); values.put( fileName, is ); } return values; } /** * retrieve a domain from the repo. This does lazy loading of the repo, so it calls reloadDomains() if not already * loaded. * * @param domainId domain to get from the repository * @return domain object */ @Override public Domain getDomain( final String domainId ) { if ( logger.isDebugEnabled() ) { logger.debug( "getDomain(" + domainId + ")" ); } if ( StringUtils.isEmpty( domainId ) ) { throw new IllegalArgumentException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0004_DOMAIN_ID_INVALID", domainId ) ); } Domain domain = null; try { // Load the domain file final RepositoryFile file = getMetadataRepositoryFile( domainId ); if ( file != null ) { if ( hasAccessFor( file ) ) { SimpleRepositoryFileData data = repository.getDataForRead( file.getId(), SimpleRepositoryFileData.class ); if ( data != null ) { domain = xmiParser.parseXmi( data.getStream() ); domain.setId( domainId ); logger.debug( "loaded domain" ); // Load any I18N bundles loadLocaleStrings( domainId, domain ); logger.debug( "loaded I18N bundles" ); } else { throw new UnifiedRepositoryException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0005_ERROR_RETRIEVING_DOMAIN", domainId, "data not found" ) ); } } else { throw new PentahoAccessControlException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0005_ERROR_RETRIEVING_DOMAIN", domainId, "access denied" ) ); } } } catch ( Exception e ) { if ( !( e instanceof UnifiedRepositoryException || e instanceof PentahoAccessControlException ) ) { throw new UnifiedRepositoryException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0005_ERROR_RETRIEVING_DOMAIN", domainId, e.getLocalizedMessage() ), e ); } } // Return return domain; } /** * return a list of all the domain ids in the repository. triggers a call to reloadDomains if necessary. * * @return the domain Ids. */ @Override public Set<String> getDomainIds() { logger.debug( "getDomainIds()" ); reloadDomainsIfNeeded(); Collection<String> domains; lock.readLock().lock(); try { domains = new ArrayList<String>( metadataMapping.getDomainIds() ); } finally { lock.readLock().unlock(); } Set<String> domainIds = new HashSet<String>( domains.size() ); for ( String domain : domains ) { if ( hasAccessFor( domain ) ) { domainIds.add( domain ); } } return domainIds; } /** * remove a domain from disk and memory. * * @param domainId */ @Override public void removeDomain( final String domainId ) { if ( logger.isDebugEnabled() ) { logger.debug( "removeDomain(" + domainId + ")" ); } if ( StringUtils.isEmpty( domainId ) ) { throw new IllegalArgumentException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0004_DOMAIN_ID_INVALID", domainId ) ); } // Get the metadata domain file RepositoryFile domainFile; Set<RepositoryFile> domainFiles; lock.writeLock().lock(); try { domainFiles = metadataMapping.getFiles( domainId ); domainFile = metadataMapping.getDomainFile( domainId ); metadataMapping.deleteDomain( domainId ); } finally { lock.writeLock().unlock(); } if ( domainFile != null ) { // it no node exists, nothing would happen getAclHelper().removeAclFor( domainFile ); } for ( final RepositoryFile file : domainFiles ) { if ( logger.isTraceEnabled() ) { logger.trace( "Deleting repository file " + toString( file ) ); } repository.deleteFile( file.getId(), true, null ); } // This invalidates any caching if ( !domainFiles.isEmpty() ) { flushDomains(); } } /** * remove a model from a domain which is stored either on a disk or memory. * * @param domainId * @param modelId */ @Override public void removeModel( final String domainId, final String modelId ) throws DomainIdNullException, DomainStorageException { if ( logger.isDebugEnabled() ) { logger.debug( "removeModel(" + domainId + ", " + modelId + ")" ); } if ( StringUtils.isEmpty( domainId ) ) { throw new IllegalArgumentException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0004_DOMAIN_ID_INVALID", domainId ) ); } if ( StringUtils.isEmpty( modelId ) ) { throw new IllegalArgumentException( messages .getErrorString( "PentahoMetadataDomainRepository.ERROR_0006_MODEL_ID_INVALID" ) ); } // Get the domain and remove the model final Domain domain = getDomain( domainId ); if ( null != domain ) { boolean found = false; final Iterator<LogicalModel> iter = domain.getLogicalModels().iterator(); while ( iter.hasNext() ) { LogicalModel model = iter.next(); if ( modelId.equals( model.getId() ) ) { iter.remove(); found = true; break; } } // Update the domain if we change it if ( found ) { try { storeDomain( domain, true ); flushDomains(); } catch ( DomainAlreadyExistsException ignored ) { // This can't happen since we have setup overwrite to true } } } } /** * reload domains from disk */ @Override public void reloadDomains() { logger.debug( "reloadDomains()" ); internalReloadDomains(); } /** * Performs the process of reloading the domain information from the repository */ private void internalReloadDomains() { lock.writeLock().lock(); try { metadataMapping.reset(); // Reload the metadata about the metadata (that was fun to say) final List<RepositoryFile> children = repository.getChildren( getMetadataDir().getId(), "*" ); if ( logger.isTraceEnabled() ) { logger.trace( "\tFound " + children.size() + " files in the repository" ); } for ( final RepositoryFile child : children ) { if ( getAclHelper().canAccess( child, READ ) ) { // Get the metadata for this file final Map<String, Serializable> fileMetadata = repository.getFileMetadata( child.getId() ); if ( fileMetadata == null || StringUtils.isEmpty( (String) fileMetadata.get( PROPERTY_NAME_DOMAIN_ID ) ) ) { if ( logger.isWarnEnabled() ) { logger.warn( messages.getString( "PentahoMetadataDomainRepository.WARN_0001_FILE_WITHOUT_METADATA", child .getName() ) ); } continue; } final String domainId = (String) fileMetadata.get( PROPERTY_NAME_DOMAIN_ID ); final String type = (String) fileMetadata.get( PROPERTY_NAME_TYPE ); final String locale = (String) fileMetadata.get( PROPERTY_NAME_LOCALE ); if ( logger.isTraceEnabled() ) { logger.trace( "\tprocessing file [type=" + type + " : domainId=" + domainId + " : locale=" + locale + "]" ); } // Save the data in the map if ( StringUtils.equals( type, TYPE_DOMAIN ) ) { metadataMapping.addDomain( domainId, child ); } else if ( StringUtils.equals( type, TYPE_LOCALE ) ) { metadataMapping.addLocale( domainId, locale, child ); } } } needToReload = false; } finally { lock.writeLock().unlock(); } } /** * flush the domains from memory */ @Override public void flushDomains() { logger.debug( "flushDomains()" ); internalReloadDomains(); } @Override public String generateRowLevelSecurityConstraint( final LogicalModel model ) { // We will let subclasses handle this issue return null; } /** * The aclHolder cannot be null unless the access type requested is ACCESS_TYPE_SCHEMA_ADMIN. */ @Override public boolean hasAccess( final int accessType, final IConcept aclHolder ) { // We will let subclasses handle this computation return true; } /** * Adds a set of properties as a locale properties file for the specified Domain ID * * @param domainId the domain ID for which this properties file will be added * @param locale the locale for which this properties file will be added * @param properties the properties to be added */ public void addLocalizationFile( final String domainId, final String locale, final Properties properties ) throws DomainStorageException { // This is safe since ByteArray streams don't have to be closed if ( null != properties ) { try { final OutputStream out = new ByteArrayOutputStream(); properties.store( out, null ); addLocalizationFile( domainId, locale, new ByteArrayInputStream( out.toString().getBytes() ), true ); } catch ( IOException e ) { throw new DomainStorageException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0008_ERROR_IN_REPOSITORY", e.getLocalizedMessage() ), e ); } } } @Override public void addLocalizationFile( final String domainId, final String locale, final InputStream inputStream, final boolean overwrite ) throws DomainStorageException { if ( logger.isDebugEnabled() ) { logger.debug( "addLocalizationFile(" + domainId + ", " + locale + ", inputStream)" ); } if ( null != inputStream ) { if ( StringUtils.isEmpty( domainId ) || StringUtils.isEmpty( locale ) ) { throw new IllegalArgumentException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0004_DOMAIN_ID_INVALID", domainId ) ); } lock.writeLock().lock(); try { // Check for duplicates final RepositoryFile localeFile = metadataMapping.getLocaleFile( domainId, locale ); if ( !overwrite && localeFile != null ) { throw new DomainStorageException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0009_LOCALE_ALREADY_EXISTS", domainId, locale ), null ); } final SimpleRepositoryFileData data = new SimpleRepositoryFileData( inputStream, DEFAULT_ENCODING, LOCALE_MIME_TYPE ); if ( localeFile == null ) { final RepositoryFile newLocaleFile = createUniqueFile( domainId, locale, data ); metadataMapping.addLocale( domainId, locale, newLocaleFile ); } else { repository.updateFile( localeFile, data, null ); } // This invalidates any cached information flushDomains(); } finally { lock.writeLock().unlock(); } } } protected RepositoryFile getMetadataDir() { final String metadataDirName = PentahoMetadataDomainRepositoryInfo.getMetadataFolderPath(); return repository.getFile( metadataDirName ); } protected void loadLocaleStrings( final String domainId, final Domain domain ) { final Map<String, RepositoryFile> localeFiles = metadataMapping.getLocaleFiles( domainId ); if ( localeFiles != null ) { for ( final String locale : localeFiles.keySet() ) { final RepositoryFile localeFile = localeFiles.get( locale ); final Properties properties = loadProperties( localeFile ); if ( logger.isTraceEnabled() ) { logger.trace( "\tLoading properties [" + domain + " : " + locale + "]" ); } localizationUtil.importLocalizedProperties( domain, properties, locale ); } } } protected Properties loadProperties( final RepositoryFile bundle ) { try { Properties properties = null; final SimpleRepositoryFileData bundleData = repository.getDataForRead( bundle.getId(), SimpleRepositoryFileData.class ); if ( bundleData != null ) { properties = new Properties(); properties.load( bundleData.getStream() ); } else { if ( logger.isWarnEnabled() ) { logger.warn( "Could not load properties from repository file: " + bundle.getName() ); } } return properties; } catch ( IOException e ) { throw new UnifiedRepositoryException( messages.getErrorString( "PentahoMetadataDomainRepository.ERROR_0008_ERROR_IN_REPOSITORY", e.getLocalizedMessage() ), e ); } } /** * Creates a new repository file (with the supplied data) and applies the proper metadata to this file. * * @param domainId the Domain id associated with this file * @param locale the locale associated with this file (or null for a domain file) * @param data the data to put in the file * @return the repository file created */ protected RepositoryFile createUniqueFile( final String domainId, final String locale, final SimpleRepositoryFileData data ) { // Generate a "unique" filename final String filename = UUID.randomUUID().toString(); // Create the new file final RepositoryFile file = repository.createFile( getMetadataDir().getId(), new RepositoryFile.Builder( filename ).build(), data, null ); // Add metadata to the file final Map<String, Serializable> metadataMap = new HashMap<String, Serializable>(); metadataMap.put( PROPERTY_NAME_DOMAIN_ID, domainId ); if ( StringUtils.isEmpty( locale ) ) { // This is a domain file metadataMap.put( PROPERTY_NAME_TYPE, TYPE_DOMAIN ); } else { // This is a locale property file metadataMap.put( PROPERTY_NAME_TYPE, TYPE_LOCALE ); metadataMap.put( PROPERTY_NAME_LOCALE, locale ); } // Update the metadata repository.setFileMetadata( file.getId(), metadataMap ); return file; } protected IUnifiedRepository getRepository() { return repository; } protected XmiParser getXmiParser() { return xmiParser; } protected RepositoryUtils getRepositoryUtils() { return repositoryUtils; } protected LocalizationUtil getLocalizationUtil() { return localizationUtil; } protected void setRepository( final IUnifiedRepository repository ) { this.repository = repository; } protected void setXmiParser( final XmiParser xmiParser ) { this.xmiParser = ( xmiParser != null ? xmiParser : new XmiParser() ); } protected void setRepositoryUtils( final RepositoryUtils repositoryUtils ) { this.repositoryUtils = ( repositoryUtils != null ? repositoryUtils : new RepositoryUtils( repository ) ); } protected void setLocalizationUtil( final LocalizationUtil localizationUtil ) { this.localizationUtil = ( localizationUtil != null ? localizationUtil : new LocalizationUtil() ); } protected String toString( final RepositoryFile file ) { try { final Map<String, Serializable> fileMetadata = repository.getFileMetadata( file.getId() ); return "[type=" + fileMetadata.get( PROPERTY_NAME_TYPE ) + " : domain=" + fileMetadata.get( PROPERTY_NAME_DOMAIN_ID ) + " : locale=" + fileMetadata.get( PROPERTY_NAME_LOCALE ) + " : filename=" + file.getName() + "]"; } catch ( Throwable ignore ) { //ignore } return "null"; } /** * Accesses the metadata mapping (with 1 retry) to find the metadata file for the specified domainId */ protected RepositoryFile getMetadataRepositoryFile( final String domainId ) { lock.readLock().lock(); RepositoryFile domainFile; try { domainFile = metadataMapping.getDomainFile( domainId ); } finally { lock.readLock().unlock(); } if ( domainFile == null ) { if ( logger.isDebugEnabled() ) { logger.debug( "Requested Domain (" + domainId + ") wasn't found in Metadata Mapping. Domain cache will be reloaded" ); } lock.writeLock().lock(); try { domainFile = metadataMapping.getDomainFile( domainId ); if ( domainFile == null ) { reloadDomainsIfNeeded(); domainFile = metadataMapping.getDomainFile( domainId ); } } finally { lock.writeLock().unlock(); } } if ( domainFile == null && logger.isDebugEnabled() ) { logger.debug( "Even after reloading all domains, the specified Domain wasn't found in the system: " + domainId ); } return domainFile; } private void reloadDomainsIfNeeded() { lock.writeLock().lock(); try { if ( needToReload ) { internalReloadDomains(); } } finally { lock.writeLock().unlock(); } } /** * Returns the MatadataInformationMap for the specified IUnifiedRepository * * @param repository the repository for which a map is specified * @return the MatadataInformationMap for the repository */ private static synchronized PentahoMetadataInformationMap getMetadataMapping( final IUnifiedRepository repository ) { PentahoMetadataInformationMap metaMap = metaMapStore.get( repository ); if ( null == metaMap ) { metaMap = new PentahoMetadataInformationMap(); metaMapStore.put( repository, metaMap ); } return metaMap; } @Override public String loadAnnotationsXml( final String domainId ) { if ( StringUtils.isBlank( domainId ) ) { return null; // exit early } try { final RepositoryFile domainFile = getMetadataRepositoryFile( domainId ); final RepositoryFile annotationFile = getRepository().getFile( resolveAnnotationsFilePath( domainFile ) ); // Load referenced annotations xml repo file SimpleRepositoryFileData data = getRepository().getDataForRead( annotationFile.getId(), SimpleRepositoryFileData.class ); return IOUtils.toString( data.getInputStream() ); // return as String } catch ( Exception e ) { getLogger().warn( "Unable to load annotations xml file for domain: " + domainId ); } return null; } @Override public void storeAnnotationsXml( String domainId, String annotationsXml ) { if ( StringUtils.isBlank( domainId ) || StringUtils.isBlank( annotationsXml ) ) { return; // exit early } RepositoryFile domainFile = getMetadataRepositoryFile( domainId ); RepositoryFile annotationsFile = getAnnotationsXmlFile( domainFile ); createOrUpdateAnnotationsXml( domainFile, annotationsFile, annotationsXml ); } public RepositoryFile getAnnotationsXmlFile( final RepositoryFile domainFile ) { if ( domainFile == null ) { return null; // exit early } RepositoryFile annotationsFile = null; try { annotationsFile = getRepository().getFile( resolveAnnotationsFilePath( domainFile ) ); } catch ( Exception e ) { getLogger().warn( "Unable to find annotations xml file for: " + domainFile.getId() ); } return annotationsFile; } public void createOrUpdateAnnotationsXml( final RepositoryFile domainFile, final RepositoryFile annotationsFile, final String annotationsXml ) { if ( domainFile == null ) { return; // exit early } try { ByteArrayInputStream in = new ByteArrayInputStream( annotationsXml.getBytes( DEFAULT_ENCODING ) ); final SimpleRepositoryFileData data = new SimpleRepositoryFileData( in, DEFAULT_ENCODING, DOMAIN_MIME_TYPE ); if ( annotationsFile == null ) { // Generate a filename based on the domainId final String filename = domainFile.getId() + ANNOTATIONS_FILE_ID_POSTFIX; // Create the new file getRepository() .createFile( getMetadataDir().getId(), new RepositoryFile.Builder( filename ).build(), data, null ); } else { getRepository().updateFile( annotationsFile, data, null ); } } catch ( Exception e ) { getLogger().warn( "Unable to save annotations xml", e ); } } protected String resolveAnnotationsFilePath( final RepositoryFile domainFile ) { if ( getMetadataDir() != null && domainFile != null ) { return getMetadataDir().getPath() + "/" + domainFile.getId() + ANNOTATIONS_FILE_ID_POSTFIX; } return null; } protected Log getLogger() { return logger; } }