001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.workspace;
017
018import java.io.BufferedReader;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.net.URL;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.Set;
031
032import javax.xml.XMLConstants;
033import javax.xml.parsers.SAXParserFactory;
034import javax.xml.validation.Schema;
035import javax.xml.validation.SchemaFactory;
036
037import org.apache.avalon.framework.configuration.Configuration;
038import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041import org.xml.sax.XMLReader;
042
043import org.ametys.runtime.servlet.RuntimeConfig;
044
045
046/**
047 * Main entry point for access to workspaces.
048 */
049public final class WorkspaceManager
050{
051    // shared instance
052    private static WorkspaceManager __manager;
053    
054    private static final String __WORKSPACE_FILENAME = "workspace.xml";
055
056    /**
057     * Cause of the deactivation of a workspace
058     */
059    public enum InactivityCause
060    {
061        /**
062         * Constant for excluded workspaces
063         */
064        EXCLUDED,
065        /**
066         * Constant for workspaces having error in their declaration
067         */
068        MISDECLARED,
069        /**
070         * Constant for workspaces having error in their workspace.xml file
071         */
072        MISCONFIGURED
073    }
074    
075    // Map<workspaceName, baseURI>
076    private Map<String, Workspace> _workspaces = new HashMap<>(); 
077    
078    private Map<String, InactivityCause> _inactiveWorkspaces = new HashMap<>();
079
080    private Logger _logger = LoggerFactory.getLogger(WorkspaceManager.class); 
081
082    
083    private WorkspaceManager()
084    {
085        // empty constructor
086    }
087    
088    /**
089     * Returns the shared instance of the <code>WorkspaceManager</code>
090     * @return the shared instance of the <code>WorkspaceManager</code>
091     */
092    public static WorkspaceManager getInstance()
093    {
094        if (__manager == null)
095        {
096            __manager = new WorkspaceManager();
097        }
098        
099        return __manager;
100    }
101    
102    /**
103     * Returns all active workspaces names
104     * @return all active workspaces names
105     */
106    public Set<String> getWorkspaceNames()
107    {
108        return Collections.unmodifiableSet(_workspaces.keySet());
109    }
110    
111    /**
112     * Returns active workspaces declarations.
113     * @return active workspaces declarations.
114     */
115    public Map<String, Workspace> getWorkspaces()
116    {
117        return Collections.unmodifiableMap(_workspaces);
118    }
119
120    /** 
121     * Return the inactive workspaces
122     * @return All the inactive workspaces
123     */
124    public Map<String, InactivityCause> getInactiveWorkspaces()
125    {
126        return Collections.unmodifiableMap(_inactiveWorkspaces);
127    }
128    
129    /**
130     * Returns a String array containing the ids of the plugins embedded in jars
131     * @return a String array containing the ids of the plugins embedded in jars
132     */
133    public Set<String> getEmbeddedWorskpacesIds()
134    {
135        return _workspaces.keySet();
136    }
137
138    /**
139     * Returns the base URI associated with the given workspace, or null if the workspace does not exist
140     * @param workspaceName the name of the working workspace
141     * @return the URI associated with the given workspace
142     */
143    public String getBaseURI(String workspaceName)
144    {
145        return _workspaces.get(workspaceName).getEmbededLocation();
146    }
147    
148    /**
149     * Returns the workspace filesystem location for the given workspace or null if the workspace is loaded from the classpath.
150     * @param workspaceName the workspace name
151     * @return the workspace location for the given workspace
152     */
153    public File getLocation(String workspaceName)
154    {
155        return _workspaces.get(workspaceName).getExternalLocation();
156    }
157
158    /**
159     * Initialize the WorkspaceManager.<br>
160     * It first looks for META-INF/runtime-workspace files in the classpath, then in the "workspaces" directory of the application.
161     * @param excludedWorkspace the excluded workspaces, as given by the RuntimeConfig
162     * @param contextPath the servlet context path
163     * @throws IOException if an error occurs while retrieving resources
164     */
165    public void init(Collection<String> excludedWorkspace, String contextPath) throws IOException
166    {
167        _workspaces.clear();
168        
169        // Begin with workspace embedded in jars
170        Enumeration<URL> workspaceResources = getClass().getClassLoader().getResources("META-INF/ametys-workspaces");
171        while (workspaceResources.hasMoreElements())
172        {
173            URL workspaceResource = workspaceResources.nextElement();
174            _initResourceWorkspace(excludedWorkspace, workspaceResource);
175        }
176
177        // Then workspace from the "<context>/workspaces" directory
178        File workspacesDir = new File(contextPath, "workspaces");
179        if (workspacesDir.exists() && workspacesDir.isDirectory())
180        {
181            for (File workspace : new File(contextPath, "workspaces").listFiles())
182            {
183                _initFileWorkspaces(workspace, workspace.getName(), excludedWorkspace);
184            }
185        }
186        
187        Map<String, File> externalWorkspaces = RuntimeConfig.getInstance().getExternalWorkspaces();
188        
189        // external workspaces
190        for (String externalWorkspace : externalWorkspaces.keySet())
191        {
192            File workspaceDir = externalWorkspaces.get(externalWorkspace);
193
194            if (workspaceDir.exists() && workspaceDir.isDirectory())
195            {
196                _initFileWorkspaces(workspaceDir, externalWorkspace, excludedWorkspace);
197            }
198            else
199            {
200                throw new RuntimeException("The configured external workspace is not an existing directory: " + workspaceDir.getAbsolutePath());
201            }
202        }
203    }
204    
205    private void _initResourceWorkspace(Collection<String> excludedWorkspace, URL workspaceResource) throws IOException
206    {
207        try (InputStream is = workspaceResource.openStream();
208             BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")))
209        {
210            String workspaceString;
211            while ((workspaceString = br.readLine()) != null)
212            {
213                int i = workspaceString.indexOf(':');
214                if (i != -1)
215                {
216                    String workspaceName = workspaceString.substring(0, i);       
217                    String workspaceBaseURI = workspaceString.substring(i + 1);
218                    
219                    if (!excludedWorkspace.contains(workspaceName))
220                    {
221                        if (_workspaces.containsKey(workspaceName))
222                        {
223                            String errorMessage = "The workspace named " + workspaceName + " already exists";
224                            _logger.error(errorMessage);
225                            throw new IllegalArgumentException(errorMessage);
226                        }
227                        
228                        URL workspaceConfigurationURL = getClass().getResource(workspaceBaseURI + "/" + __WORKSPACE_FILENAME); 
229                        if (workspaceConfigurationURL == null || getClass().getResource(workspaceBaseURI + "/" + __WORKSPACE_FILENAME) == null)
230                        {
231                            if (_logger.isWarnEnabled())
232                            {
233                                _logger.warn("A workspace '" + workspaceName + "' is declared in a library, but files '" + __WORKSPACE_FILENAME + "' and/or 'sitemap.xmap' are missing at '" + workspaceBaseURI + "'. Workspace will be ignored.");
234                            }
235                            _inactiveWorkspaces.put(workspaceName, InactivityCause.MISDECLARED);
236                            return;
237                        }
238
239                        Configuration workspaceConfiguration = null;
240                        try (InputStream is2 = workspaceConfigurationURL.openStream())
241                        {
242                            workspaceConfiguration = _getConfigurationFromStream(workspaceName, is2, workspaceBaseURI);
243                        }
244                        
245                        if (workspaceConfiguration != null)
246                        {
247                            Workspace workspace = new Workspace(workspaceName, workspaceBaseURI);
248                            workspace.configure(workspaceConfiguration);
249                            
250                            _workspaces.put(workspaceName, workspace);
251                            
252                            if (_logger.isInfoEnabled())
253                            {
254                                _logger.info("Workspace '" + workspaceName + "' registered at '" + workspaceBaseURI + "'");
255                            }
256                        }
257                        else
258                        {
259                            _inactiveWorkspaces.put(workspaceName, InactivityCause.MISCONFIGURED);
260                        }
261                    }
262                    else
263                    {
264                        _inactiveWorkspaces.put(workspaceName, InactivityCause.EXCLUDED);
265                    }
266                }
267            }
268        }
269    }
270    
271    private void _initFileWorkspaces(File workspaceFile, String workspaceName, Collection<String> excludedWorkspace) throws IOException
272    {
273        File workspaceConfigurationFile = new File(workspaceFile, __WORKSPACE_FILENAME);
274        if (excludedWorkspace.contains(workspaceName))
275        {
276            _inactiveWorkspaces.put(workspaceName, InactivityCause.EXCLUDED);
277        }
278        else if (workspaceFile.exists() && workspaceFile.isDirectory() && workspaceConfigurationFile.exists() && new File(workspaceFile, "sitemap.xmap").exists())
279        {
280            if (_workspaces.containsKey(workspaceName))
281            {
282                String errorMessage = "The workspace named " + workspaceFile.getName() + " already exists";
283                _logger.error(errorMessage);
284                throw new IllegalArgumentException(errorMessage);
285            }
286            
287            Configuration workspaceConfiguration = null;
288            try (InputStream is = new FileInputStream(workspaceConfigurationFile))
289            {
290                workspaceConfiguration = _getConfigurationFromStream(workspaceName, is, workspaceConfigurationFile.getAbsolutePath());
291            }
292            
293            if (workspaceConfiguration != null)
294            {
295                Workspace workspace = new Workspace(workspaceName, workspaceFile);
296                workspace.configure(workspaceConfiguration);
297                
298                _workspaces.put(workspaceName, workspace);
299                
300                if (_logger.isInfoEnabled())
301                {
302                    _logger.info("Workspace '" + workspaceName + "' registered at '" + workspaceConfigurationFile.getAbsolutePath() + "'");
303                }
304            }
305            else
306            {
307                _inactiveWorkspaces.put(workspaceName, InactivityCause.MISCONFIGURED);
308            }
309        }  
310        else
311        {
312            if (_logger.isWarnEnabled())
313            {
314                _logger.warn("Workspace '" + workspaceName + "' registered at '" + workspaceFile.getAbsolutePath() + "' has no '" + __WORKSPACE_FILENAME + "' file or no 'sitemap.xmap' file.");
315            }
316
317            _inactiveWorkspaces.put(workspaceName, InactivityCause.MISDECLARED);
318        }
319    }
320    
321    private Configuration _getConfigurationFromStream(String workspaceName, InputStream is, String path)
322    {
323        try
324        {
325            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
326            URL schemaURL = getClass().getResource("workspace-4.0.xsd");
327            Schema schema = schemaFactory.newSchema(schemaURL);
328            SAXParserFactory factory = SAXParserFactory.newInstance();
329            factory.setNamespaceAware(true);
330            factory.setSchema(schema);
331            XMLReader reader = factory.newSAXParser().getXMLReader();
332            DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader);
333            
334            return confBuilder.build(is, path);
335        }
336        catch (Exception e)
337        {
338            _logger.error("Unable to access to workspace '" + workspaceName + "' at " + path, e);
339            return null;
340        }
341    }
342
343}