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