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