001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     bstefanescu
018 */
019package org.nuxeo.runtime.model.impl;
020
021import java.io.ByteArrayInputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.Collections;
026import java.util.HashSet;
027import java.util.Set;
028import java.util.concurrent.locks.ReadWriteLock;
029import java.util.concurrent.locks.ReentrantReadWriteLock;
030
031import javax.xml.parsers.DocumentBuilder;
032import javax.xml.parsers.DocumentBuilderFactory;
033import javax.xml.parsers.ParserConfigurationException;
034
035import org.apache.commons.io.FileUtils;
036import org.nuxeo.common.Environment;
037import org.nuxeo.runtime.model.RegistrationInfo;
038import org.nuxeo.runtime.model.RuntimeContext;
039import org.nuxeo.runtime.osgi.OSGiRuntimeService;
040import org.osgi.framework.Bundle;
041import org.w3c.dom.Document;
042import org.w3c.dom.Element;
043import org.xml.sax.SAXException;
044
045/**
046 * Manage persistent components. Persistent components are located in ${nxserver_data_dir}/components directory, and can
047 * be dynamically removed or registered. After framework startup (after the application was completely started) the
048 * persistent components are deployed. The layout of the components directory is the following:
049 *
050 * <pre>
051 * components/
052 *     component1.xml
053 *     component2.xml
054 *     ...
055 *     bundle_symbolicName1/
056 *         component1.xml
057 *         component2.xml
058 *         ...
059 *     bundle_symbolicName1/
060 *         ...
061 *     ...
062 * </pre>
063 *
064 * If components are put directly under the root then they will be deployed in the runtime bundle context. If they are
065 * put in a directory having as name the symbolicName of a bundle in the system, then the component will be deployed in
066 * that bundle context.
067 * <p>
068 * Any files not ending with .xml are ignored. Any directory that doesn't match a bundle symbolic name will be ignored
069 * too.
070 * <p>
071 * Dynamic components must use the following name convention: (it is not mandatory but it is recommended)
072 * <ul>
073 * <li>Components deployed in root directory must use as name the file name without the .xml extension.
074 * <li>Components deployed in a bundle directory must use the relative file path without the .xml extensions.
075 * </ul>
076 * Examples: Given the following component files: <code>components/mycomp1.xml</code> and
077 * <code>components/mybundle/mycomp2.xml</code> the name for <code>mycomp1</code> must be: <code>comp1</code> and for
078 * <code>mycomp2</code> must be <code>mybundle/mycomp2</code>
079 * <p>
080 * This service is working only with {@link OSGiRuntimeService}
081 *
082 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
083 */
084public class ComponentPersistence {
085
086    protected final File root; // a directory to keep exploded extensions
087
088    protected final RuntimeContext sysrc;
089
090    protected final OSGiRuntimeService runtime;
091
092    protected final ReadWriteLock fileLock;
093
094    protected final Set<RegistrationInfo> persistedComponents;
095
096    public ComponentPersistence(OSGiRuntimeService runtime) {
097        this.runtime = runtime;
098        root = new File(Environment.getDefault().getData(), "components");
099        fileLock = new ReentrantReadWriteLock();
100        sysrc = runtime.getContext();
101        persistedComponents = Collections.synchronizedSet(new HashSet<>());
102    }
103
104    public File getRoot() {
105        return root;
106    }
107
108    public final RuntimeContext getContext(String symbolicName) {
109        if (symbolicName == null) {
110            return sysrc;
111        }
112        Bundle bundle = runtime.getBundle(symbolicName);
113        if (bundle == null) {
114            return null;
115        }
116        return runtime.createContext(bundle);
117    }
118
119    protected void deploy(RuntimeContext rc, File file) throws IOException {
120        RegistrationInfoImpl ri = (RegistrationInfoImpl) rc.deploy(file.toURI().toURL());
121        ri.isPersistent = true;
122    }
123
124    public void loadPersistedComponents() throws IOException {
125        File[] files = root.listFiles();
126        if (files != null) {
127            for (File file : files) {
128                if (file.isDirectory()) {
129                    RuntimeContext rc = getContext(file.getName());
130                    if (rc != null) {
131                        loadPersistedComponents(rc, file);
132                    }
133                } else if (file.isFile() && file.getName().endsWith(".xml")) {
134                    deploy(sysrc, file);
135                }
136            }
137        }
138    }
139
140    public void loadPersistedComponents(RuntimeContext rc, File root) throws IOException {
141        File[] files = root.listFiles();
142        if (files != null) {
143            for (File file : files) {
144                if (file.isFile() && file.getName().endsWith(".xml")) {
145                    deploy(rc, file);
146                }
147            }
148        }
149    }
150
151    public void loadPersistedComponent(File file) throws IOException {
152        file = file.getCanonicalFile();
153        if (file.isFile() && file.getName().endsWith(".xml")) {
154            File parent = file.getParentFile();
155            if (root.equals(parent)) {
156                deploy(sysrc, file);
157                return;
158            } else {
159                String symbolicName = parent.getName();
160                parent = parent.getParentFile();
161                if (root.equals(parent)) {
162                    RuntimeContext rc = getContext(symbolicName);
163                    if (rc != null) {
164                        deploy(rc, file);
165                        return;
166                    }
167                }
168            }
169        }
170        throw new IllegalArgumentException("Invalid component file location or bundle not found");
171    }
172
173    public Document loadXml(File file) throws IOException {
174        byte[] bytes = safeReadFile(file);
175        return loadXml(new ByteArrayInputStream(bytes));
176    }
177
178    public static Document loadXml(InputStream in) {
179        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
180        factory.setNamespaceAware(true);
181        try {
182            DocumentBuilder builder = factory.newDocumentBuilder();
183            return builder.parse(in);
184        } catch (SAXException | IOException | ParserConfigurationException e) {
185            throw new RuntimeException(e);
186        }
187    }
188
189    public void createComponent(byte[] bytes) throws IOException {
190        createComponent(bytes, true);
191    }
192
193    public synchronized void createComponent(byte[] bytes, boolean isPersistent) throws IOException {
194        Document doc = loadXml(new ByteArrayInputStream(bytes));
195        Element root = doc.getDocumentElement();
196        String name = root.getAttribute("name");
197        int p = name.indexOf(':');
198        if (p > -1) {
199            name = name.substring(p + 1);
200        }
201        p = name.indexOf('/');
202        String owner = null;
203        if (p > -1) {
204            owner = name.substring(0, p);
205        }
206        DefaultRuntimeContext rc = (DefaultRuntimeContext) getContext(owner);
207        File file = new File(this.root, name + ".xml");
208        if (!isPersistent) {
209            file.deleteOnExit();
210        }
211        file.getParentFile().mkdirs();
212        safeWriteFile(bytes, file);
213        rc.deploy(file.toURI().toURL());
214    }
215
216    public synchronized boolean removeComponent(String compName) throws IOException {
217        String path = compName + ".xml";
218        File file = new File(root, path);
219        if (!file.isFile()) {
220            return false;
221        }
222        int p = compName.indexOf('/');
223        String owner = null;
224        if (p > -1) {
225            owner = compName.substring(0, p);
226        }
227        DefaultRuntimeContext rc = (DefaultRuntimeContext) getContext(owner);
228        rc.undeploy(file.toURI().toURL());
229        file.delete();
230        return true;
231    }
232
233    protected void safeWriteFile(byte[] bytes, File file) throws IOException {
234        fileLock.writeLock().lock();
235        try {
236            FileUtils.writeByteArrayToFile(file, bytes);
237        } finally {
238            fileLock.writeLock().unlock();
239        }
240    }
241
242    protected byte[] safeReadFile(File file) throws IOException {
243        fileLock.readLock().lock();
244        try {
245            return FileUtils.readFileToByteArray(file);
246        } finally {
247            fileLock.readLock().unlock();
248        }
249    }
250
251}