View Javadoc

1   /*
2    * Copyright (c) 2007, James Leigh All rights reserved.
3    * 
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    * 
7    * - Redistributions of source code must retain the above copyright notice, this
8    *   list of conditions and the following disclaimer.
9    * - Redistributions in binary form must reproduce the above copyright notice,
10   *   this list of conditions and the following disclaimer in the documentation
11   *   and/or other materials provided with the distribution. 
12   * - Neither the name of the openrdf.org nor the names of its contributors may
13   *   be used to endorse or promote products derived from this software without
14   *   specific prior written permission.
15   * 
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26   * POSSIBILITY OF SUCH DAMAGE.
27   * 
28   */
29  package org.openrdf.elmo.codegen;
30  
31  import groovy.lang.GroovyClassLoader;
32  import groovy.util.GroovyScriptEngine;
33  
34  import java.io.File;
35  import java.io.FileNotFoundException;
36  import java.io.FileOutputStream;
37  import java.io.FileWriter;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.OutputStream;
41  import java.io.PrintStream;
42  import java.io.Writer;
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  import java.net.URLClassLoader;
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.Enumeration;
49  import java.util.HashMap;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Properties;
53  import java.util.jar.JarEntry;
54  import java.util.jar.JarFile;
55  import java.util.jar.JarOutputStream;
56  
57  import org.apache.commons.cli.CommandLine;
58  import org.apache.commons.cli.GnuParser;
59  import org.apache.commons.cli.HelpFormatter;
60  import org.apache.commons.cli.Option;
61  import org.apache.commons.cli.Options;
62  import org.apache.commons.cli.ParseException;
63  import org.openrdf.elmo.LiteralManager;
64  import org.openrdf.elmo.sesame.SesameLiteralManager;
65  import org.openrdf.model.Literal;
66  import org.openrdf.model.Statement;
67  import org.openrdf.model.URI;
68  import org.openrdf.model.ValueFactory;
69  import org.openrdf.model.impl.URIImpl;
70  import org.openrdf.model.vocabulary.OWL;
71  import org.openrdf.model.vocabulary.RDF;
72  import org.openrdf.repository.Repository;
73  import org.openrdf.repository.RepositoryConnection;
74  import org.openrdf.repository.RepositoryException;
75  import org.openrdf.repository.RepositoryResult;
76  import org.openrdf.repository.sail.SailRepository;
77  import org.openrdf.rio.RDFFormat;
78  import org.openrdf.rio.RDFParseException;
79  import org.openrdf.rio.RDFWriter;
80  import org.openrdf.rio.Rio;
81  import org.openrdf.sail.memory.MemoryStore;
82  import org.slf4j.Logger;
83  import org.slf4j.LoggerFactory;
84  
85  /**
86   * A Facade to CodeGenerator and OwlGenerator classes. This class provides a
87   * simpler interface to create concept packages and build ontologies. Unlike the
88   * composed classes, this class reads and creates jar packages.
89   * 
90   * @author James Leigh
91   * 
92   */
93  public class OntologyConverter {
94  
95  	private static final String META_INF_ELMO_ROLES = "META-INF/org.openrdf.elmo.roles";
96  
97  	private static final String META_INF_ELMO_LITERALS = "META-INF/org.openrdf.elmo.literals";
98  
99  	private static final String META_INF_ONTOLOGIES = "META-INF/org.openrdf.elmo.ontologies";
100 
101 	private static final Options options = new Options();
102 	static {
103 		Option pkg = new Option("b", "bind", true,
104 				"Binds the package name and ontology URI together");
105 		pkg.setArgName("package=uri");
106 		Option jar = new Option("j", "jar", true,
107 				"filename where the jar will be saved");
108 		jar.setArgName("jar file");
109 		Option file = new Option("r", "rdf", true,
110 				"filename where the rdf ontology will be saved");
111 		file.setArgName("RDF file");
112 		Option prefix = new Option("p", "prefix", true,
113 				"prefix the property names with namespace prefix (default is true)");
114 		prefix.setArgName("true|false");
115 		Option baseClass = new Option("e", "extends", true,
116 				"super class that all concepts should extend");
117 		baseClass.setArgName("full class name");
118 		options.addOption(baseClass);
119 		options.addOption(prefix);
120 		options.addOption("h", "help", false, "print this message");
121 		options.addOption(pkg);
122 		options.addOption(jar);
123 		options.addOption(file);
124 	}
125 
126 	public static void main(String[] args) throws Exception {
127 		try {
128 			CommandLine line = new GnuParser().parse(options, args);
129 			if (line.hasOption('h')) {
130 				HelpFormatter formatter = new HelpFormatter();
131 				String cmdLineSyntax = "codegen [options] [ontology | jar]...";
132 				String header = "[ontology | jar]... are a list of RDF and jar files that should be imported before converting.";
133 				formatter.printHelp(cmdLineSyntax, header, options, "");
134 				return;
135 			}
136 			if (!line.hasOption('b'))
137 				throw new ParseException("Required bind option missing");
138 			if (!line.hasOption('j') && !line.hasOption('r'))
139 				throw new ParseException("Required jar or rdf option missing");
140 			if (line.hasOption('j') && line.hasOption('r'))
141 				throw new ParseException(
142 						"Only one jar or rdf option can be present");
143 			OntologyConverter converter = new OntologyConverter();
144 			boolean prefix = Boolean.parseBoolean(line.getOptionValue('p',
145 					"true"));
146 			converter.setPropertyNamesPrefixed(prefix);
147 			if (line.hasOption('e')) {
148 				converter.setBaseClasses(line.getOptionValues('e'));
149 			}
150 			findJars(line.getArgs(), 0, converter);
151 			findRdfSources(line.getArgs(), 0, converter);
152 			for (String value : line.getOptionValues('b')) {
153 				String[] split = value.split("=", 2);
154 				if (split.length != 2) {
155 					throw new ParseException("Invalid bind option: " + value);
156 				}
157 				converter.addOntology(new URIImpl(split[1]), split[0]);
158 			}
159 			converter.init();
160 			if (line.hasOption('j')) {
161 				converter.createClasses(new File(line.getOptionValue('j')));
162 			} else {
163 				converter.createOntology(new File(line.getOptionValue('r')));
164 			}
165 			return;
166 		} catch (ParseException exp) {
167 			System.err.println(exp.getMessage());
168 			System.exit(1);
169 		}
170 	}
171 
172 	private static void findRdfSources(String[] args, int offset,
173 			OntologyConverter converter) throws MalformedURLException {
174 		for (int i = offset; i < args.length; i++) {
175 			if (args[i].endsWith(".jar"))
176 				continue;
177 			URL url;
178 			File file = new File(args[i]);
179 			if (file.exists()) {
180 				url = file.toURI().toURL();
181 			} else {
182 				url = new URL(args[i]);
183 			}
184 			converter.addRdfSource(url);
185 		}
186 	}
187 
188 	private static void findJars(String[] args, int offset,
189 			OntologyConverter converter) throws MalformedURLException {
190 		for (int i = offset; i < args.length; i++) {
191 			URL url;
192 			File file = new File(args[i]);
193 			if (file.exists()) {
194 				url = file.toURI().toURL();
195 			} else {
196 				url = new URL(args[i]);
197 			}
198 			if (args[i].endsWith(".jar")) {
199 				converter.addJar(url);
200 			}
201 		}
202 	}
203 
204 	final Logger logger = LoggerFactory.getLogger(OntologyConverter.class);
205 
206 	private boolean importJarOntologies = true;
207 
208 	private List<URL> jars = new ArrayList<URL>();
209 
210 	private List<URL> rdfSources = new ArrayList<URL>();
211 
212 	private Map<String, String> namespaces = new HashMap<String, String>();
213 
214 	private Map<URI, String> ontologies = new HashMap<URI, String>();
215 
216 	private Repository repository;
217 
218 	private URLClassLoader cl;
219 
220 	private boolean propertyNamesPrefixed = true;
221 
222 	private String[] baseClasses;
223 
224 	/**
225 	 * If the ontologies bundled with the included jars should be imported.
226 	 * 
227 	 * @return <code>true</code> if the ontology will be imported.
228 	 */
229 	public boolean isImportJarOntologies() {
230 		return importJarOntologies;
231 	}
232 
233 	/**
234 	 * If the ontologies bundled with the included jars should be imported.
235 	 * 
236 	 * @param importJarOntologies
237 	 *            <code>true</code> if the ontology will be imported.
238 	 */
239 	public void setImportJarOntologies(boolean importJarOntologies) {
240 		this.importJarOntologies = importJarOntologies;
241 	}
242 
243 	/**
244 	 * If the property names should be prefixed.
245 	 * 
246 	 * @return <code>true</code> if the property names will be prefixed.
247 	 */
248 	public boolean isPropertyNamesPrefixed() {
249 		return propertyNamesPrefixed;
250 	}
251 
252 	/**
253 	 * If the property names should be prefixed.
254 	 * 
255 	 * @param propertyNamesPrefixed
256 	 *            <code>true</code> if the property names will be prefixed.
257 	 */
258 	public void setPropertyNamesPrefixed(boolean propertyNamesPrefixed) {
259 		this.propertyNamesPrefixed = propertyNamesPrefixed;
260 	}
261 
262 	/**
263 	 * Array of Java Class names that all concepts will extend.
264 	 * 
265 	 * @return Array of Java Class names that all concepts will extend.
266 	 */
267 	public String[] getBaseClasses() {
268 		return baseClasses;
269 	}
270 
271 	/**
272 	 * Array of Java Class names that all concepts will extend.
273 	 * 
274 	 * @param strings
275 	 */
276 	public void setBaseClasses(String[] strings) {
277 		this.baseClasses = strings;
278 	}
279 
280 	/**
281 	 * Add a jar of classes to include in the class-path.
282 	 * 
283 	 * @param url
284 	 */
285 	public void addJar(URL url) {
286 		jars.add(url);
287 	}
288 
289 	/**
290 	 * Adds an RDF file to the local repository.
291 	 * 
292 	 * @param url
293 	 */
294 	public void addRdfSource(URL url) {
295 		rdfSources.add(url);
296 	}
297 
298 	/**
299 	 * Set the prefix that should be used for this ontology namespace.
300 	 * 
301 	 * @param prefix
302 	 * @param namespace
303 	 */
304 	public void setNamespace(String prefix, String namespace) {
305 		namespaces.put(prefix, namespace);
306 	}
307 
308 	/**
309 	 * Binds this ontology with the package name.
310 	 * 
311 	 * @param ontology
312 	 * @param pkgName
313 	 */
314 	public void addOntology(URI ontology, String pkgName) {
315 		ontologies.put(ontology, pkgName);
316 	}
317 
318 	/**
319 	 * Create the local repository and load the RDF files.
320 	 * 
321 	 * @throws Exception
322 	 */
323 	public void init() throws Exception {
324 		cl = createClassLoader(jars);
325 		repository = createRepository(cl);
326 		for (URL url : rdfSources) {
327 			loadOntology(repository, url);
328 		}
329 	}
330 
331 	/**
332 	 * Generate an OWL ontology from the JavaBeans in the included jars.
333 	 * 
334 	 * @param rdfOutputFile
335 	 * @throws Exception
336 	 * @see {@link #addOntology(URI, String)}
337 	 * @see {@link #addJar(URL)}
338 	 */
339 	public void createOntology(File rdfOutputFile) throws Exception {
340 		List<Class<?>> beans = new ArrayList<Class<?>>();
341 		if (ontologies.isEmpty()) {
342 			beans.addAll(findBeans(null, jars, cl));
343 		} else {
344 			for (String packageName : ontologies.values()) {
345 				beans.addAll(findBeans(packageName, jars, cl));
346 			}
347 		}
348 		ValueFactory vf = repository.getValueFactory();
349 		SesameLiteralManager manager = new SesameLiteralManager(vf);
350 		manager.setClassLoader(cl);
351 		createOntology(beans, manager, rdfOutputFile);
352 	}
353 
354 	/**
355 	 * Generate Elmo concept Java classes from the ontology in the local
356 	 * repository.
357 	 * 
358 	 * @param jarOutputFile
359 	 * @throws Exception
360 	 * @see {@link #addOntology(URI, String)}
361 	 * @see {@link #addRdfSource(URL)}
362 	 */
363 	public void createClasses(File jarOutputFile) throws Exception {
364 		createJar(repository, cl, jarOutputFile);
365 	}
366 
367 	protected Repository createRepository() throws RepositoryException {
368 		Repository repository = new SailRepository(new MemoryStore());
369 		repository.initialize();
370 		return repository;
371 	}
372 
373 	private URLClassLoader createClassLoader(List<URL> importJars)
374 			throws MalformedURLException {
375 		Thread thread = Thread.currentThread();
376 		ClassLoader cl = thread.getContextClassLoader();
377 		if (cl == null) {
378 			cl = OntologyConverter.class.getClassLoader();
379 		}
380 		URL[] classpath = importJars.toArray(new URL[0]);
381 		if (cl instanceof URLClassLoader) {
382 			URL[] urls = ((URLClassLoader) cl).getURLs();
383 			URL[] jars = classpath;
384 			classpath = new URL[jars.length + urls.length];
385 			System.arraycopy(jars, 0, classpath, 0, jars.length);
386 			System.arraycopy(urls, 0, classpath, jars.length, urls.length);
387 		}
388 		return URLClassLoader.newInstance(classpath, cl);
389 	}
390 
391 	private Repository createRepository(ClassLoader cl)
392 			throws RepositoryException, IOException, RDFParseException {
393 		Repository repository = createRepository();
394 		RepositoryConnection conn = repository.getConnection();
395 		try {
396 			for (Map.Entry<String, String> e : namespaces.entrySet()) {
397 				conn.setNamespace(e.getKey(), e.getValue());
398 			}
399 		} finally {
400 			conn.close();
401 		}
402 		if (importJarOntologies) {
403 			for (String owl : loadOntologyList(cl)) {
404 				URL url = cl.getResource(owl);
405 				loadOntology(repository, url);
406 			}
407 		}
408 		return repository;
409 	}
410 
411 	@SuppressWarnings("unchecked")
412 	private Collection<String> loadOntologyList(ClassLoader cl)
413 			throws IOException {
414 		Properties ontologies = new Properties();
415 		String name = "META-INF/org.openrdf.elmo.ontologies";
416 		Enumeration<URL> resources = cl.getResources(name);
417 		while (resources.hasMoreElements()) {
418 			URL url = resources.nextElement();
419 			ontologies.load(url.openStream());
420 		}
421 		Collection<?> list = ontologies.keySet();
422 		return (Collection<String>) list;
423 	}
424 
425 	private void loadOntology(Repository repository, URL url)
426 			throws RepositoryException, IOException, RDFParseException {
427 		String filename = url.toString();
428 		RDFFormat format = formatForFileName(filename);
429 		RepositoryConnection conn = repository.getConnection();
430 		try {
431 			conn.add(url, "", format);
432 		} finally {
433 			conn.close();
434 		}
435 	}
436 
437 	private RDFFormat formatForFileName(String filename) {
438 		RDFFormat format = RDFFormat.forFileName(filename);
439 		if (format != null)
440 			return format;
441 		if (filename.endsWith(".owl"))
442 			return RDFFormat.RDFXML;
443 		throw new IllegalArgumentException("Unknow RDF format for " + filename);
444 	}
445 
446 	private List<Class<?>> findBeans(String pkgName, List<URL> urls,
447 			URLClassLoader cl) throws Exception {
448 		List<Class<?>> beans = new ArrayList<Class<?>>();
449 		for (URL jar : urls) {
450 			JarFile file = new JarFile(jar.getFile());
451 			Enumeration<JarEntry> entries = file.entries();
452 			while (entries.hasMoreElements()) {
453 				JarEntry entry = entries.nextElement();
454 				String name = entry.getName();
455 				if (name.contains("-") || !name.endsWith(".class"))
456 					continue;
457 				name = name.replace('/', '.').replace('\\', '.');
458 				if (pkgName == null || name.startsWith(pkgName)
459 						&& name.substring(pkgName.length() + 1).contains(".")) {
460 					name = name.replaceAll(".class$", "");
461 					beans.add(Class.forName(name, true, cl));
462 				}
463 			}
464 		}
465 		return beans;
466 	}
467 
468 	private void createOntology(List<Class<?>> beans,
469 			LiteralManager<URI, Literal> manager, File output) throws Exception {
470 		RDFFormat format = formatForFileName(output.getName());
471 		Writer out = new FileWriter(output);
472 		try {
473 			RDFWriter writer = Rio.createWriter(format, out);
474 			OwlGenerator gen = new OwlGenerator();
475 			gen.setLiteralManager(manager);
476 			writer.startRDF();
477 			writer.handleNamespace("owl", OWL.NAMESPACE);
478 			for (Map.Entry<URI, String> e : ontologies.entrySet()) {
479 				URI ontology = e.getKey();
480 				String pkgName = e.getValue();
481 				String prefix = pkgName.substring(pkgName.lastIndexOf('.') + 1);
482 				String namespace = ontology.toString();
483 				if (!namespace.endsWith("/") && !namespace.endsWith("#"))
484 					namespace += '#';
485 				writer.handleNamespace(prefix, namespace);
486 				gen.setNamespace(pkgName, namespace);
487 			}
488 			gen.exportOntology(beans, writer);
489 			writer.endRDF();
490 		} finally {
491 			out.close();
492 		}
493 	}
494 
495 	private void createJar(Repository repository, URLClassLoader cl, File output)
496 			throws Exception {
497 		FileSourceCodeHandler handler = new FileSourceCodeHandler();
498 		generateSourceCode(repository, cl, handler);
499 		if (handler.getClasses().isEmpty())
500 			throw new IllegalArgumentException("No classes found - Try a different ontology URI.");
501 		JavaCompiler javac = new JavaCompiler();
502 		javac.compile(handler.getTarget(), handler.getClasses(), cl);
503 		packageJar(output, handler);
504 	}
505 
506 	private void generateSourceCode(Repository repository, ClassLoader cl,
507 			FileSourceCodeHandler handler) throws Exception {
508 		CodeGenerator gen = new CodeGenerator();
509 		gen.setPropertyNamesPrefixed(propertyNamesPrefixed);
510 		if (baseClasses != null) {
511 			List<Class<?>> base = new ArrayList<Class<?>>();
512 			for (String bc : baseClasses) {
513 				base.add(Class.forName(bc, true, cl));
514 			}
515 			gen.setBaseClasses(base.toArray(new Class<?>[base.size()]));
516 		}
517 		gen.setRepository(repository);
518 		ClassLoader loader = new GroovyClassLoader(cl);
519 		gen.setGroovyScriptEngine(new GroovyScriptEngine(gen, loader));
520 		for (Map.Entry<URI, String> e : ontologies.entrySet()) {
521 			gen.addOntologyPackage(e.getKey(), e.getValue());
522 		}
523 		gen.init();
524 		gen.exportSourceCode(handler);
525 	}
526 
527 	private void packageJar(File output, FileSourceCodeHandler handler)
528 			throws Exception {
529 		File dir = handler.getTarget();
530 		FileOutputStream stream = new FileOutputStream(output);
531 		JarOutputStream jar = new JarOutputStream(stream);
532 		try {
533 			packClasses(dir, handler.getClasses(), jar);
534 			printRoles(handler.getAnnotatedClasses(), jar);
535 			printLiterals(handler.getConcreteClasses(), jar);
536 			packOntologies(rdfSources, jar);
537 		} finally {
538 			jar.close();
539 			stream.close();
540 		}
541 	}
542 
543 	private void packClasses(File dir, List<String> classes, JarOutputStream jar)
544 			throws IOException, FileNotFoundException {
545 		for (String name : classes) {
546 			String basename = name.replace('.', '/');
547 			String source = basename + ".java";
548 			String binary = basename + ".class";
549 			File sourceFile = new File(dir, source);
550 			File binaryFile = new File(dir, binary);
551 			jar.putNextEntry(new JarEntry(source));
552 			copyInto(sourceFile.toURI().toURL(), jar);
553 			sourceFile.delete();
554 			if (binaryFile.exists()) {
555 				jar.putNextEntry(new JarEntry(binary));
556 				copyInto(binaryFile.toURI().toURL(), jar);
557 				binaryFile.delete();
558 			}
559 		}
560 	}
561 
562 	private void copyInto(URL source, OutputStream out)
563 			throws FileNotFoundException, IOException {
564 		logger.debug("Packaging {}", source);
565 		InputStream in = source.openStream();
566 		try {
567 			int read;
568 			byte[] buf = new byte[512];
569 			while ((read = in.read(buf)) > 0) {
570 				out.write(buf, 0, read);
571 			}
572 		} finally {
573 			in.close();
574 		}
575 	}
576 
577 	private void printRoles(List<String> roles, JarOutputStream jar)
578 			throws IOException {
579 		jar.putNextEntry(new JarEntry(META_INF_ELMO_ROLES));
580 		PrintStream out = new PrintStream(jar);
581 		for (String name : roles) {
582 			out.println(name);
583 		}
584 		out.flush();
585 	}
586 
587 	private void printLiterals(List<String> roles, JarOutputStream jar)
588 			throws IOException {
589 		jar.putNextEntry(new JarEntry(META_INF_ELMO_LITERALS));
590 		PrintStream out = new PrintStream(jar);
591 		for (String name : roles) {
592 			out.println(name);
593 		}
594 		out.flush();
595 	}
596 
597 	private void packOntologies(List<URL> rdfSources, JarOutputStream jar)
598 			throws RepositoryException, RDFParseException, IOException {
599 		Map<String, URI> ontologies = new HashMap<String, URI>();
600 		for (URL rdf : rdfSources) {
601 			String path = "META-INF/ontologies/";
602 			path += new File(rdf.getPath()).getName();
603 			URI ontology = findOntology(rdf);
604 			if (ontology != null) {
605 				ontologies.put(path, ontology);
606 				jar.putNextEntry(new JarEntry(path));
607 				copyInto(rdf, jar);
608 			}
609 		}
610 		if (ontologies.isEmpty())
611 			return;
612 		jar.putNextEntry(new JarEntry(META_INF_ONTOLOGIES));
613 		PrintStream out = new PrintStream(jar);
614 		for (Map.Entry<String, URI> e : ontologies.entrySet()) {
615 			out.print(e.getKey());
616 			out.print("\t=\t");
617 			out.println(e.getValue().toString());
618 		}
619 		out.flush();
620 	}
621 
622 	private URI findOntology(URL rdf) throws RepositoryException,
623 			RDFParseException, IOException {
624 		Repository repository = createRepository();
625 		loadOntology(repository, rdf);
626 		RepositoryConnection conn = repository.getConnection();
627 		try {
628 			RepositoryResult<Statement> stmts;
629 			stmts = conn.getStatements(null, RDF.TYPE, OWL.ONTOLOGY, true);
630 			try {
631 				if (stmts.hasNext())
632 					return (URI) stmts.next().getSubject();
633 				return null;
634 			} finally {
635 				stmts.close();
636 			}
637 		} finally {
638 			conn.clear();
639 			conn.close();
640 		}
641 	}
642 
643 }