001/*
002 * (C) Copyright 2006-2010 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Bogdan Stefanescu
016 *     Thierry Delprat
017 */
018package org.nuxeo.apidoc.introspection;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.FileReader;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.InputStreamReader;
027import java.io.Reader;
028import java.io.Writer;
029import java.lang.annotation.Annotation;
030import java.lang.reflect.Field;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.Enumeration;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.PropertyResourceBundle;
038import java.util.zip.ZipEntry;
039import java.util.zip.ZipFile;
040
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.ParserConfigurationException;
044import javax.xml.xpath.XPath;
045import javax.xml.xpath.XPathConstants;
046import javax.xml.xpath.XPathException;
047import javax.xml.xpath.XPathFactory;
048
049import org.apache.commons.logging.Log;
050import org.apache.commons.logging.LogFactory;
051import org.nuxeo.apidoc.api.ComponentInfo;
052import org.nuxeo.apidoc.documentation.DocumentationHelper;
053import org.nuxeo.common.utils.FileUtils;
054import org.nuxeo.ecm.core.api.NuxeoException;
055import org.nuxeo.osgi.BundleImpl;
056import org.nuxeo.runtime.RuntimeService;
057import org.nuxeo.runtime.api.Framework;
058import org.nuxeo.runtime.model.Extension;
059import org.nuxeo.runtime.model.ExtensionPoint;
060import org.nuxeo.runtime.model.RegistrationInfo;
061import org.osgi.framework.Bundle;
062import org.w3c.dom.Document;
063import org.xml.sax.SAXException;
064
065/**
066 * The entry point to the server runtime introspection To build a description of the current running server you need to
067 * create a {@link ServerInfo} object using the method {@link #build(String, String)}.
068 * <p>
069 * Example
070 *
071 * <pre>
072 * ServerInfo info = ServerInfo.build();
073 * </pre>
074 *
075 * The server name and version will be fetched form the runtime properties: <code>org.nuxeo.ecm.product.name</code> and
076 * <code>org.nuxeo.ecm.product.version</code> If you ant to use another name and version just call
077 * {@link #build(String, String)} instead to build your server information.
078 * <p>
079 * After building a <code>ServerInfo</code> object you can start browsing the bundles deployed on the server by calling
080 * {@link #getBundles()} or fetch a specific bundle given its symbolic name {@link #getBundle(String)}.
081 * <p>
082 * To write down the server information as XML use {@link #toXML(Writer)} and to read it back use
083 * {@link #fromXML(Reader)}.
084 * <p>
085 * Example:
086 *
087 * <pre>
088 * ServerInfo info = ServerInfo.build();
089 * BundleInfo binfo =info.getBundle("org.nuxeo.runtime");
090 * System.out.println("Bundle Id: "+binfo.getBundleId());
091 * System.out.println("File Name: "+binfo.getFileName());
092 * System.out.println("Manifest: "+ binfo.getManifest());
093 * for (ComponentInfo cinfo : binfo.getComponents()) {
094 *   System.out.println("Component: "+cinfo.getName());
095 *   System.out.println(cinfo.getDocumentation());
096 *   // find extension points provided by this component
097 *   for (ExtensionPointInfo xpi : cinfo.getExtensionPoints()) {
098 *     System.out.println("Extension point: "+xpi.getName());
099 *     System.out.println("Accepted contribution classes: "+Arrays.asList(xpi.getTypes()));
100 *     // find contributed extensions to this extension point:
101 *
102 *   }
103 *   // find contribution provided by this component
104 *   for (ExtensionInfo xi : cinfo.getExtensions()) {
105 *      System.out.println("Extension: "+xi.getId()+" to "+xi.getExtensionPoint());
106 *      System.out.println(xi.getDocumentation());
107 *      ...
108 *   }
109 * }
110 * </pre>
111 */
112public class ServerInfo {
113
114    private static final Log log = LogFactory.getLog(ServerInfo.class);
115
116    public static final String META_INF_MANIFEST_MF = "META-INF/MANIFEST.MF";
117
118    public static final String POM_XML = "pom.xml";
119
120    public static final String POM_PROPERTIES = "pom.properties";
121
122    protected static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
123
124    protected static final XPathFactory xpathFactory = XPathFactory.newInstance();
125
126    protected final String name;
127
128    protected final String version;
129
130    protected final Map<String, BundleInfoImpl> bundles = new HashMap<String, BundleInfoImpl>();
131
132    protected final List<Class<?>> allSpi = new ArrayList<Class<?>>();
133
134    public ServerInfo(String name, String version) {
135        this.name = name;
136        this.version = version;
137    }
138
139    public String getName() {
140        return name;
141    }
142
143    public String getVersion() {
144        return version;
145    }
146
147    public Collection<BundleInfoImpl> getBundles() {
148        return bundles.values();
149    }
150
151    public void addBundle(BundleInfoImpl bundle) {
152        bundles.put(bundle.getId(), bundle);
153    }
154
155    public void addBundle(Collection<BundleInfoImpl> bundles) {
156        for (BundleInfoImpl bundle : bundles) {
157            this.bundles.put(bundle.getId(), bundle);
158        }
159    }
160
161    public BundleInfoImpl getBundle(String id) {
162        return bundles.get(id);
163    }
164
165    public static ServerInfo build() {
166        return build(Framework.getProperty("org.nuxeo.ecm.product.name", "Nuxeo"),
167                Framework.getProperty("org.nuxeo.ecm.product.version", "unknown"));
168    }
169
170    protected static BundleInfoImpl computeBundleInfo(Bundle bundle) {
171        RuntimeService runtime = Framework.getRuntime();
172        BundleInfoImpl binfo = new BundleInfoImpl(bundle.getSymbolicName());
173        binfo.setFileName(runtime.getBundleFile(bundle).getName());
174        binfo.setLocation(bundle.getLocation());
175        if (!(bundle instanceof BundleImpl)) {
176            return binfo;
177        }
178        BundleImpl nxBundle = (BundleImpl) bundle;
179        File jarFile = nxBundle.getBundleFile().getFile();
180        if (jarFile == null) {
181            return binfo;
182        }
183        try {
184            if (jarFile.isDirectory()) {
185                // directory: run from Eclipse in unit tests
186                // .../nuxeo-runtime/nuxeo-runtime/bin
187                // or sometimes
188                // .../nuxeo-runtime/nuxeo-runtime/bin/main
189                File manifest = new File(jarFile, META_INF_MANIFEST_MF);
190                if (manifest.exists()) {
191                    InputStream is = new FileInputStream(manifest);
192                    String mf = FileUtils.read(is);
193                    binfo.setManifest(mf);
194                }
195                // find and parse pom.xml
196                File up = new File(jarFile, "..");
197                File pom = new File(up, POM_XML);
198                if (!pom.exists()) {
199                    pom = new File(new File(up, ".."), POM_XML);
200                    if (!pom.exists()) {
201                        pom = null;
202                    }
203                }
204                if (pom != null) {
205                    DocumentBuilder b = documentBuilderFactory.newDocumentBuilder();
206                    Document doc = b.parse(new FileInputStream(pom));
207                    XPath xpath = xpathFactory.newXPath();
208                    String groupId = (String) xpath.evaluate("//project/groupId", doc, XPathConstants.STRING);
209                    if ("".equals(groupId)) {
210                        groupId = (String) xpath.evaluate("//project/parent/groupId", doc, XPathConstants.STRING);
211                    }
212                    String artifactId = (String) xpath.evaluate("//project/artifactId", doc, XPathConstants.STRING);
213                    if ("".equals(artifactId)) {
214                        artifactId = (String) xpath.evaluate("//project/parent/artifactId", doc, XPathConstants.STRING);
215                    }
216                    String version = (String) xpath.evaluate("//project/version", doc, XPathConstants.STRING);
217                    if ("".equals(version)) {
218                        version = (String) xpath.evaluate("//project/parent/version", doc, XPathConstants.STRING);
219                    }
220                    binfo.setArtifactId(artifactId);
221                    binfo.setGroupId(groupId);
222                    binfo.setArtifactVersion(version);
223                }
224            } else {
225                ZipFile zFile = new ZipFile(jarFile);
226                ZipEntry mfEntry = zFile.getEntry(META_INF_MANIFEST_MF);
227                if (mfEntry != null) {
228                    InputStream mfStream = zFile.getInputStream(mfEntry);
229                    String mf = FileUtils.read(mfStream);
230                    binfo.setManifest(mf);
231                }
232                Enumeration<? extends ZipEntry> entries = zFile.entries();
233                while (entries.hasMoreElements()) {
234                    ZipEntry entry = entries.nextElement();
235                    if (entry.getName().endsWith(POM_PROPERTIES)) {
236                        InputStream is = zFile.getInputStream(entry);
237                        PropertyResourceBundle prb = new PropertyResourceBundle(is);
238                        String groupId = prb.getString("groupId");
239                        String artifactId = prb.getString("artifactId");
240                        String version = prb.getString("version");
241                        binfo.setArtifactId(artifactId);
242                        binfo.setGroupId(groupId);
243                        binfo.setArtifactVersion(version);
244                        is.close();
245                        break;
246                    }
247                }
248                zFile.close();
249                zFile = new ZipFile(jarFile);
250                EmbeddedDocExtractor.extractEmbeddedDoc(zFile, binfo);
251                zFile.close();
252            }
253        } catch (IOException | ParserConfigurationException | SAXException | XPathException | NuxeoException e) {
254            log.error(e, e);
255        }
256        return binfo;
257    }
258
259    protected static List<Class<?>> getSPI(Class<?> klass) {
260        List<Class<?>> spi = new ArrayList<Class<?>>();
261        for (Field field : klass.getDeclaredFields()) {
262            String cName = field.getType().getCanonicalName();
263            if (cName.startsWith("org.nuxeo")) {
264                // remove XObjects
265                Class<?> fieldClass = field.getType();
266                Annotation[] annotations = fieldClass.getDeclaredAnnotations();
267                if (annotations.length == 0) {
268                    spi.add(fieldClass);
269                }
270            }
271        }
272        return spi;
273    }
274
275    public static ServerInfo build(String name, String version) {
276        RuntimeService runtime = Framework.getRuntime();
277        ServerInfo server = new ServerInfo(name, version);
278        BundleInfoImpl configVirtualBundle = new BundleInfoImpl("org.nuxeo.ecm.config");
279        server.addBundle(configVirtualBundle);
280
281        Map<String, ExtensionPointInfoImpl> xpRegistry = new HashMap<String, ExtensionPointInfoImpl>();
282        List<ExtensionInfoImpl> contribRegistry = new ArrayList<ExtensionInfoImpl>();
283
284        Collection<RegistrationInfo> registrations = runtime.getComponentManager().getRegistrations();
285
286        for (RegistrationInfo ri : registrations) {
287            String cname = ri.getName().getName();
288            Bundle bundle = ri.getContext().getBundle();
289            BundleInfoImpl binfo = null;
290
291            if (bundle == null) {
292                binfo = configVirtualBundle;
293            } else {
294                String symName = bundle.getSymbolicName();
295                if (symName == null) {
296                    log.error("No symbolic name found for bundle " + cname);
297                    continue;
298                }
299                // avoids duplicating/overriding the bundles
300                if (server.bundles.containsKey(bundle.getSymbolicName())) {
301                    binfo = server.bundles.get(bundle.getSymbolicName());
302                } else {
303                    binfo = computeBundleInfo(bundle);
304                }
305            }
306
307            // TODO binfo.setRequirements(requirements);
308            ComponentInfoImpl component = new ComponentInfoImpl(binfo, cname);
309            if (ri.getExtensionPoints() != null) {
310                for (ExtensionPoint xp : ri.getExtensionPoints()) {
311                    ExtensionPointInfoImpl xpinfo = new ExtensionPointInfoImpl(component, xp.getName());
312                    Class<?>[] ctypes = xp.getContributions();
313                    String[] descriptors = new String[ctypes.length];
314
315                    for (int i = 0; i < ctypes.length; i++) {
316                        descriptors[i] = ctypes[i].getCanonicalName();
317                        List<Class<?>> spi = getSPI(ctypes[i]);
318                        xpinfo.addSpi(spi);
319                        server.allSpi.addAll(spi);
320                    }
321                    xpinfo.setDescriptors(descriptors);
322                    xpinfo.setDocumentation(xp.getDocumentation());
323                    xpRegistry.put(xpinfo.getId(), xpinfo);
324                    component.addExtensionPoint(xpinfo);
325                }
326            }
327
328            component.setXmlFileUrl(ri.getXmlFileUrl());
329
330            if (ri.getProvidedServiceNames() != null) {
331                for (String serviceName : ri.getProvidedServiceNames()) {
332                    component.addService(serviceName);
333                }
334            }
335
336            if (ri.getExtensions() != null) {
337                for (Extension xt : ri.getExtensions()) {
338                    ExtensionInfoImpl xtinfo = new ExtensionInfoImpl(component, xt.getExtensionPoint());
339                    xtinfo.setTargetComponentName(xt.getTargetComponent());
340                    xtinfo.setContribution(xt.getContributions());
341                    xtinfo.setDocumentation(xt.getDocumentation());
342                    xtinfo.setXml(DocumentationHelper.secureXML(xt.toXML()));
343
344                    contribRegistry.add(xtinfo);
345
346                    component.addExtension(xtinfo);
347                }
348            }
349
350            component.setComponentClass(ri.getImplementation());
351            component.setDocumentation(ri.getDocumentation());
352
353            binfo.addComponent(component);
354            server.addBundle(binfo);
355        }
356
357        // now register the bundles that contains no components !!!
358        Bundle[] allbundles = runtime.getContext().getBundle().getBundleContext().getBundles();
359        for (Bundle bundle : allbundles) {
360            if (!server.bundles.containsKey(bundle.getSymbolicName())) {
361                BundleInfoImpl bi = computeBundleInfo(bundle);
362                server.addBundle(bi);
363            }
364        }
365
366        // associate contrib to XP
367        for (ExtensionInfoImpl contrib : contribRegistry) {
368            String xp = contrib.getExtensionPoint();
369            ExtensionPointInfoImpl ep = xpRegistry.get(xp);
370            if (ep != null) {
371                ep.addExtension(contrib);
372            }
373        }
374
375        return server;
376    }
377
378    public void toXML(Writer writer) throws IOException {
379        XMLWriter xw = new XMLWriter(writer, 4);
380        xw.start();
381        xw.element("server").attr("name", name).attr("version", version).start();
382        for (BundleInfoImpl bundle : bundles.values()) {
383            xw.element("bundle").attr("id", bundle.bundleId).start();
384            xw.element("fileName").content(bundle.fileName);
385            // TODO requirements
386            for (ComponentInfo component : bundle.getComponents()) {
387                xw.element("component").attr("id", component.getId()).start();
388                // for (ExtensionPointInfo xp : component.getExtensionPoints())
389                // { }
390                // for (ExtensionInfo xt : component.getExtensions()) { }
391                xw.close();
392            }
393            xw.close();
394        }
395        xw.close();
396        xw.close();
397    }
398
399    public static ServerInfo fromXML(File file) throws IOException {
400        InputStreamReader reader = new FileReader(file);
401        try {
402            return fromXML(reader);
403        } finally {
404            reader.close();
405        }
406    }
407
408    public static ServerInfo fromXML(Reader reader) {
409        return null;
410    }
411
412    public List<Class<?>> getAllSpi() {
413        return allSpi;
414    }
415
416}