001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.rdf.owl2java;
031
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.Map;
039
040import org.apache.commons.io.FileUtils;
041import org.kohsuke.args4j.Argument;
042import org.kohsuke.args4j.CmdLineException;
043import org.kohsuke.args4j.CmdLineParser;
044import org.kohsuke.args4j.Option;
045import org.openrdf.model.URI;
046import org.openrdf.query.BindingSet;
047import org.openrdf.query.MalformedQueryException;
048import org.openrdf.query.QueryEvaluationException;
049import org.openrdf.query.QueryLanguage;
050import org.openrdf.query.TupleQuery;
051import org.openrdf.query.TupleQueryResult;
052import org.openrdf.repository.Repository;
053import org.openrdf.repository.RepositoryConnection;
054import org.openrdf.repository.RepositoryException;
055import org.openrdf.repository.sail.SailRepository;
056import org.openrdf.rio.RDFFormat;
057import org.openrdf.rio.RDFParseException;
058import org.openrdf.sail.memory.MemoryStore;
059
060import com.google.common.base.Charsets;
061import com.google.common.io.Resources;
062
063/**
064 * The main class of the OWL 2 Java tool. This class provides a main method that
065 * will convert an OWL/RDFS file to a set of Java code files that compile to
066 * provide an object representation of the ontology.
067 * 
068 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
069 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
070 * @created 29 Oct 2012
071 * @version $Author$, $Revision$, $Date$
072 */
073public class Generator
074{
075        /**
076         * Options for the generation of the Java classes.
077         * 
078         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
079         * @created 30 Oct 2012
080         * @version $Author$, $Revision$, $Date$
081         */
082        public static class GeneratorOptions implements Cloneable
083        {
084                /** The RDF file to convert */
085                @Argument(metaVar = "RDF-FILE", index = 0, usage = "The RDF file to convert", required = true)
086                public String rdfFile;
087
088                /** The target directory for the output */
089                @Argument(
090                                metaVar = "TARGET-DIR",
091                                index = 1,
092                                usage = "The output directory for the generated class files",
093                                required = true)
094                public String targetDirectory;
095
096                /** Whether to generate @@Predicate annotations. */
097                @Option(name = "-annotate", aliases = "-a", usage = "Generate @Predicate annotations")
098                public boolean generateAnnotations = true;
099
100                /** Whether to flatten the properties in the class structure */
101                @Option(name = "-flatten", aliases = "-f", usage = "Flatten the properties in the generated classes")
102                public boolean flattenClassStructure = false;
103
104                /** Whether to put the implementation files in a separate package */
105                @Option(name = "-separate", aliases = "-s", usage = "Put implementations in a separate package")
106                public boolean separateImplementations = true;
107
108                /** Whether to create a pom.xml file for the output files */
109                @Option(name = "-maven", aliases = "-m", usage = "Create a Maven project with this groupId", metaVar = "GROUP-ID")
110                public String mavenProject = null;
111
112                /** The artifact identifier for the maven project, if -maven is used */
113                @Option(
114                                name = "-artifactId",
115                                usage = "Specify the artifact identifier for the maven project",
116                                metaVar = "ARTIFACT-ID")
117                public String mavenArtifactId = "generated-rdf";
118
119                /** The version number for the maven project, if -maven is used */
120                @Option(name = "-version", usage = "Specify the version for the maven project", metaVar = "VERSION-NUMBER")
121                public String mavenVersionNumber = "1.0";
122
123                /** If a mavenParent is to be added to the pom.xml, the GAV is put here */
124                @Option(
125                                name = "-mavenParent",
126                                aliases = "-p",
127                                usage = "The mavenParent artifact GAV to add to the pom.xml (g:a:v)")
128                public String mavenParent = null;
129
130                /** Skip generation of the maven pom */
131                @Option(name = "-skipPom", usage = "Skip generation of the maven pom", required = false)
132                public boolean skipPom = false;
133
134                @Override
135                public Object clone() throws CloneNotSupportedException {
136                        return super.clone();
137                }
138        }
139
140        /**
141         * Useful little debug method that will print out all the triples for a
142         * given URI from the given repository.
143         * 
144         * @param uri
145         *            The URI
146         * @param conn
147         *            The connection
148         */
149        protected static void debugAllTriples(final URI uri, final RepositoryConnection conn)
150        {
151                try
152                {
153                        // SPARQL query to get the properties and property comments
154                        final String query = "SELECT ?uri ?pred ?obj WHERE { ?uri ?pred ?obj. } ";
155
156                        // Prepare the query
157                        final TupleQuery preparedQuery = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);
158
159                        // Execute the query
160                        final TupleQueryResult res = preparedQuery.evaluate();
161
162                        System.out.println("----------------------------------------------");
163                        System.out.println("Triples for " + uri);
164                        System.out.println("----------------------------------------------");
165
166                        // Loop through the results
167                        while (res.hasNext())
168                        {
169                                final BindingSet bindingSet = res.next();
170                                System.out.println("( " +
171                                                bindingSet.getBinding("uri").getValue() + " " +
172                                                bindingSet.getBinding("pred").getValue() + " " +
173                                                bindingSet.getBinding("obj").getValue() + " )"
174                                                );
175                        }
176
177                        res.close();
178
179                        System.out.println("----------------------------------------------\n");
180                } catch (final RepositoryException e)
181                {
182                        e.printStackTrace();
183                } catch (final MalformedQueryException e)
184                {
185                        e.printStackTrace();
186                } catch (final QueryEvaluationException e)
187                {
188                        e.printStackTrace();
189                }
190        }
191
192        /**
193         * Creates a cache of mappings that map URIs to package names.
194         * 
195         * @param classes
196         *            The list of classes to generate the package names for
197         * @return A map of class URI to package name
198         */
199        protected static Map<URI, String> generatePackageMappings(
200                        final Collection<ClassDef> classes)
201        {
202                final Map<URI, String> packageMapping = new HashMap<URI, String>();
203
204                for (final ClassDef cd : classes)
205                {
206                        if (!packageMapping.containsKey(cd.uri))
207                        {
208                                packageMapping.put(cd.uri, Generator.getPackageName(cd.uri));
209                        }
210                }
211                return packageMapping;
212        }
213
214        /**
215         * From the given URI will attempt to create a java package name by
216         * reversing all the elements and separating with dots.
217         * 
218         * @param uri
219         *            The URI to get a package name for.
220         * @return The Java package name
221         */
222        protected static String getPackageName(final URI uri)
223        {
224                return Generator.getPackageName(uri, true);
225        }
226
227        /**
228         * From the given URI will attempt to create a java package name by
229         * reversing all the elements and separating with dots.
230         * 
231         * @param uri
232         *            The URI to get a package name for.
233         * @param removeWWW
234         *            Whether to remove www. parts of URIs
235         * @return The Java package name
236         */
237        protected static String getPackageName(final URI uri, final boolean removeWWW)
238        {
239                String ns = uri.getNamespace();
240
241                // Remove the protocol
242                if (ns.contains("//"))
243                        ns = ns.substring(ns.indexOf("//") + 2);
244
245                // If there's no path component, return it as it is
246                if (!ns.contains("/"))
247                        return ns;
248
249                // Get the path component
250                String last = ns.substring(ns.indexOf("/") + 1);
251
252                // Remove any anchors
253                if (last.contains("#"))
254                        last = last.substring(0, last.indexOf("#"));
255
256                // Remove any file extensions
257                if (last.contains("."))
258                        last = last.substring(0, last.indexOf("."));
259
260                // Replace all the path separators by dots
261                last = last.replace("/", ".");
262
263                // Remove invalid characters
264                // Replace dashes with underscores
265                last = last.replace("-", "_");
266
267                // Get the server part (the bit we're going to reverse)
268                String serverName = ns.substring(0, ns.indexOf("/"));
269
270                // Split the server name and reverse it.
271                final String[] parts = serverName.split("\\.");
272                serverName = "";
273                int count = 0;
274                for (int i = parts.length - 1; i >= 0; i--)
275                {
276                        // Replace any invalid characters with underscores
277                        if (parts[i].charAt(0) < 65 || parts[i].charAt(0) > 122)
278                                serverName += "_";
279
280                        if (!removeWWW || !parts[i].equals("www"))
281                        {
282                                if (count != 0 && i != 0)
283                                        serverName += ".";
284                                serverName += parts[i];
285                                count++;
286                        }
287                }
288
289                // We'll also need to replace any invalid characters (or names) with
290                // new names
291                String lastBit = "";
292                if (last.indexOf(".") == -1)
293                        lastBit = last;
294                else
295                {
296                        for (final String s : last.split("\\."))
297                        {
298                                lastBit += ".";
299                                if (s.charAt(0) < 65 || s.charAt(0) > 122)
300                                        lastBit += "_" + s;
301                                else
302                                        lastBit += s;
303                        }
304                }
305
306                return serverName + lastBit;
307        }
308
309        /**
310         * Creates a pom.xml file in the given directory that contains the necessary
311         * dependencies to make the source files compile. It uses a template pom.xml
312         * that is in the resource directory.
313         * 
314         * @param targetDir
315         *            The target directory
316         * @param groupId
317         *            The groupId of the maven artifact
318         * @param artifactId
319         *            The artifactId of the maven project
320         * @param versionNumber
321         *            The version number of the maven project
322         * @param parentGAV
323         *            The GAV of the mavenParent project
324         */
325        private static void createPOM(final File targetDir, final String groupId,
326                        final String artifactId, final String versionNumber, final String parentGAV)
327        {
328                try
329                {
330                        String s = Resources.toString(Resources.getResource("pom.xml"), Charsets.UTF_8);
331                        s = Generator.replaceCodes(groupId, artifactId, versionNumber, s);
332
333                        // Determine if we need to add the parent element into the pom
334                        if (parentGAV != null)
335                        {
336                                String ps = Resources.toString(Resources.getResource("pom_xml_parent.xml"), Charsets.UTF_8);
337                                final String[] splits = parentGAV.split(":");
338                                ps = Generator.replaceCodes(splits[0], splits[1], splits[2], ps);
339                                s = s.replaceAll("\\{!p!\\}", ps);
340                        }
341                        else {
342                                s = s.replaceAll("\\{!p!\\}", "");
343                        }
344
345                        FileUtils.writeStringToFile(new File(targetDir, "pom.xml"), s, "UTF-8");
346                } catch (final IOException e)
347                {
348                        e.printStackTrace();
349                }
350        }
351
352        /**
353         * Replaces GAV codes in the given string.
354         * 
355         * @param groupId
356         *            The group identifier
357         * @param artifactId
358         *            The artifact identifier
359         * @param versionNumber
360         *            The version number
361         * @param s
362         *            The string to replace them in
363         * @return The string with the codes replaced
364         */
365        public static String replaceCodes(final String groupId,
366                        final String artifactId, final String versionNumber, String s)
367        {
368                s = s.replaceAll("\\{!g!\\}", groupId);
369                s = s.replaceAll("\\{!a!\\}", artifactId);
370                s = s.replaceAll("\\{!v!\\}", versionNumber);
371                return s;
372        }
373
374        /**
375         * Returns the Java type name for a given URI
376         * 
377         * @param s
378         *            The URI
379         * @return The Java type name
380         */
381        protected static String getTypeName(final URI s)
382        {
383                return s.getLocalName().substring(0, 1).toUpperCase() + s.getLocalName().substring(1);
384        }
385
386        /**
387         * Generate the classes for the RDF information that's read from the given
388         * input stream.
389         * 
390         * @param is
391         *            The input stream to find the RDF description
392         * @param go
393         *            The options for the generator
394         * @throws RepositoryException
395         *             If the repository cannot be created
396         * @throws IOException
397         *             If the InputStream cannot be read
398         * @throws RDFParseException
399         *             If the RDF is malformed
400         * @throws QueryEvaluationException
401         *             If the query for classes fails
402         * @throws MalformedQueryException
403         *             If the query for classes fails
404         */
405        public static void generate(final InputStream is, final GeneratorOptions go)
406                        throws RepositoryException, RDFParseException, IOException,
407                        MalformedQueryException, QueryEvaluationException
408        {
409                // If we're going to create a Maven project, we'll put all the source
410                // files into a src directory, so we need to alter the target directory.
411                if (go.mavenProject != null && !go.skipPom)
412                {
413                        // Create the pom.xml file
414                        Generator.createPOM(new File(go.targetDirectory), go.mavenProject,
415                                        go.mavenArtifactId, go.mavenVersionNumber, go.mavenParent);
416
417                        go.targetDirectory = go.targetDirectory +
418                                        File.separator + "src" + File.separator + "main" +
419                                        File.separator + "java";
420                        new File(go.targetDirectory).mkdirs();
421                }
422
423                // Create a new memory store into which we'll plonk all the RDF
424                final Repository repository = new SailRepository(new MemoryStore());
425                repository.initialize();
426
427                // Plonk all the RDF into the store
428                final RepositoryConnection conn = repository.getConnection();
429                conn.add(is, "", RDFFormat.RDFXML);
430
431                // Now we'll get all the classes from the ontology
432                final Map<URI, ClassDef> classes = ClassDef.loadClasses(go, conn);
433
434                // Try to generate the package mappings for the classes
435                final Map<URI, String> pkgs = Generator.generatePackageMappings(
436                                classes.values());
437
438                // Now we'll go through each of the class definitions and generate
439                // interfaces and classes
440                for (final ClassDef cd : classes.values())
441                {
442                        cd.generateInterface(new File(go.targetDirectory), pkgs, classes);
443                        cd.generateClass(new File(go.targetDirectory), pkgs, classes,
444                                        go.flattenClassStructure, go.generateAnnotations,
445                                        go.separateImplementations);
446                }
447        }
448
449        /**
450         * 
451         * @param args
452         * @throws RepositoryException
453         */
454        public static void main(final String[] args) throws RepositoryException
455        {
456                final GeneratorOptions go = new GeneratorOptions();
457                final CmdLineParser parser = new CmdLineParser(go);
458
459                try
460                {
461                        parser.parseArgument(args);
462                } catch (final CmdLineException e)
463                {
464                        System.err.println(e.getMessage());
465                        System.err.println("java Generator RDF-FILE TARGET-DIR [options]");
466                        parser.printUsage(System.err);
467                        System.exit(1);
468                }
469
470                final File rdfFile = new File(go.rdfFile);
471                final File targetDir = new File(go.targetDirectory);
472
473                if (!rdfFile.exists())
474                {
475                        System.out.println("The RDF file does not exist: " + rdfFile);
476                        System.exit(1);
477                }
478
479                if (!targetDir.exists())
480                {
481                        System.out.println("The target directory does not exist: "
482                                        + targetDir);
483                        System.exit(1);
484                }
485
486                try
487                {
488                        Generator.generate(new FileInputStream(rdfFile), go);
489                } catch (final Exception e)
490                {
491                        e.printStackTrace();
492                }
493        }
494}