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}