/*******************************************************************************
* Copyright (c) 2011 Knowledge Computing Corp.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Karl M. Davis (Knowledge Computing Corp.) - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.properties.editor.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* <p>
* Provides utility methods useful for discovering APT services.
* </p>
* <p>
* Please note that most of this code was copied from the <code>org.eclipse.jdt.apt.core</code> plugin's
* <code>org.eclipse.jdt.apt.core.internal.JarFactoryContainer</code> class.
* </p>
*
* @author karldavis
*/
public class AnnotationServiceLocator {
/**
* The name of the {@link Class} used to load/create annotation processors in Java 5.
*/
private static final String JAVA5_FACTORY_NAME = "com.sun.mirror.apt.AnnotationProcessorFactory"; //$NON-NLS-1$
/**
* The name of the {@link Class} used to load/create annotation processors in Java 6 and later.
*/
private static final String JAVA6_FACTORY_NAME = "javax.annotation.processing.Processor"; //$NON-NLS-1$
/**
* List of jar file entries within <code>META-INF/services</code> that specify auto-loadable service providers.
*/
private static final String[] APT_SERVICES = {JAVA5_FACTORY_NAME, JAVA6_FACTORY_NAME};
/**
* Given a JAR file, get the names of any auto-loadable Java 5-style or Java 6-style annotation processor
* implementations provided by the JAR. The information is based on the Sun <a
* href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Service%20Provider"> Jar Service Provider spec</a>:
* the jar file contains a META-INF/services directory; that directory contains text files named according to the
* desired interfaces; and each file contains the names of the classes implementing the specified service. The files
* may also contain whitespace (which is to be ignored). The '#' character indicates the beginning of a line comment,
* also to be ignored. Implied but not stated in the spec is that this routine also ignores anything after the first
* nonwhitespace token on a line.
*
* @param jar the <code>.jar</code> {@link File} to inspect for annotation processor services
* @return the {@link Set} of auto-loadable Java 5-style or Java 6-style annotation processor {@link ServiceEntry}s
* provided by the specified JAR, or an empty {@link Set} if no such {@link ServiceEntry}s are found
*/
public static Set<ServiceEntry> getAptServiceEntries(File jar) throws IOException {
// Sanity checks:
if(jar == null)
throw new IllegalArgumentException(String.format("Null %s.", File.class));
if(!jar.exists())
throw new IllegalArgumentException(String.format("Specified file does not exist: %s", jar.getAbsolutePath()));
if(!jar.canRead())
throw new IllegalArgumentException(String.format("Specified file not readable: %s", jar.getAbsolutePath()));
Set<ServiceEntry> serviceEntries = new HashSet<ServiceEntry>();
JarFile jarFile = null;
try {
jarFile = new JarFile(jar);
for(String serviceName : APT_SERVICES) {
String providerName = "META-INF/services/" + serviceName; //$NON-NLS-1$
// Get the service provider def file out of the jar.
JarEntry provider = jarFile.getJarEntry(providerName);
if(provider == null) {
continue;
}
// Extract classnames from the service provider def file.
InputStream is = jarFile.getInputStream(provider);
Set<ServiceEntry> serviceFileEntries = readServiceProvider(serviceName, is);
serviceEntries.addAll(serviceFileEntries);
}
return serviceEntries;
} finally {
try {
if(jarFile != null)
jarFile.close();
} catch(IOException ioe) {
}
}
}
/**
* Read service classnames from a service provider definition.
*
* @param serviceName the name of the service that <code>servicesDeclarationFile</code> contains entries for
* @param servicesDeclarationFile an {@link InputStream} for the <code>META-INF/services</code> file to load
* {@link ServiceEntry}s from
* @return the {@link Set} of {@link ServiceEntry}s that were found in the specified <code>META-INF/services</code>
* file, or an empty {@link Set} if no entries were found in the file
* @see http://download.oracle.com/javase/1.4.2/docs/guide/sound/programmer_guide/chapter13.html
*/
private static Set<ServiceEntry> readServiceProvider(String serviceName, InputStream servicesDeclarationFile)
throws IOException {
Set<ServiceEntry> serviceEntries = new HashSet<ServiceEntry>();
BufferedReader servicesReader = null;
try {
servicesReader = new BufferedReader(new InputStreamReader(servicesDeclarationFile, "UTF-8")); //$NON-NLS-1$
for(String line = servicesReader.readLine(); line != null; line = servicesReader.readLine()) {
// hack off any comments
int iComment = line.indexOf('#');
if(iComment >= 0) {
line = line.substring(0, iComment);
}
// add the first non-whitespace token to the list
final String[] tokens = line.split("\\s", 2); //$NON-NLS-1$
if(tokens[0].length() > 0) {
ServiceEntry serviceEntry = new ServiceEntry(serviceName, tokens[0]);
serviceEntries.add(serviceEntry);
}
}
return serviceEntries;
} finally {
if(servicesReader != null)
try {
servicesReader.close();
} catch(IOException ioe) {
}
}
}
/**
* Represents a single SPI entry.
*/
public static final class ServiceEntry {
private final String serviceName;
private final String serviceProviderClassName;
/**
* Constructs a new {@link ServiceEntry} instance.
*
* @param serviceName the name of the service that the provider implements
* @param serviceProviderClassName the {@link Class} name of the service provider represented by this
* {@link ServiceEntry}
*/
public ServiceEntry(String serviceName, String serviceProviderClassName) {
this.serviceName = serviceName;
this.serviceProviderClassName = serviceProviderClassName;
}
/**
* @return the name of the service that the provider implements
*/
public String getServiceName() {
return this.serviceName;
}
/**
* @return the {@link Class} name of the service provider represented by this {@link ServiceEntry}
*/
public String getServiceProviderClassName() {
return this.serviceProviderClassName;
}
}
}