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