package ring.compiler; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import org.python.core.PyObject; import org.python.util.PythonInterpreter; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import ring.main.RingModule; import ring.nrapi.xml.XMLConverter; import ring.persistence.Persistable; import ring.python.Interpreter; /** * Provides methods for packing and unpacking RingMUD .mud files. A .mud file * is actually a fancy zip file, similar to a Java jar file. A compiled MUD * can be deployed to the existing MUD server, or run in "development" mode, where * the database is not used. Instead, everything will be loaded into memory so the * MUD can be tested easily. * @author projectmoon * */ public class Compiler implements RingModule { private String mudProjectDir; private int errorCount; private int stripIndex; private MUDFile mudFile = null; @Override public void execute(String[] args) { //Read some options System.out.println("Compiling mud in " + args[0] + "..."); mudProjectDir = args[0]; //Validate that necessary files exist. validateFileExistence(); //Create the temporary mud file. try { mudFile = generateFile(args[0]); } catch (IOException e) { System.err.println("Could not generate mudfile: " + e); return; } //Validate mud properties. validateProperties(); //Convert data/*.py to XML convertPython(); //Validate data/*.xml validateDocuments(); //Validate ID uniqueness. validateIDs(); //Validate python? if (errorCount == 0) { outputFile(); } else { System.out.println(); System.out.println(errorCount + " error(s) found. Please fix them and recompile."); } } @Override public boolean usesDatabase() { return false; } /** * The following files MUST be present for a mud to actually run: * ./mud.properties * */ private void validateFileExistence() { String propsPath = mudProjectDir + File.separator + "mud.properties"; File propsFile = new File(propsPath); if (!propsFile.exists()) { severeError("mud.properties does not exist in directory."); } } private void validateProperties() { String mudName = mudFile.getName(); String mudVersion = mudFile.getVersion(); //Name and version cannot be blank. if (mudName.equals("")) { error("mud.properties", "You must specify the \"name\" property."); } if (mudVersion.equals("")) { error("mud.properties", "You must specify the \"version\" property."); } } private void convertPython() { try { PythonInterpreter interp = Interpreter.INSTANCE.getInterpreter(); //Provide the DocumentInfo class to all python-defined data. InputStream documentInfoStream = Interpreter.INSTANCE.getInternalScript("compiler.py"); interp.execfile(documentInfoStream); List<FileEntry> entriesToRemove = new ArrayList<FileEntry>(); List<FileEntry> entriesToAdd = new ArrayList<FileEntry>(); for (FileEntry entry : mudFile.getEntries("data")) { if (entry.getEntryName().endsWith(".py")) { //Add document info object to the interpreter. interp.exec("__document__ = DocumentInfo()"); //Execute script interp.execfile(new FileInputStream(entry.getFile())); //Then convert to XML List<Persistable> persistables = XMLConverter.getPersistables(); //More efficient to have one string, rather than create it //in each iteration below. This will be the file name of the python file, //minus the .py extension. String fileRoot = entry.getEntryName(); fileRoot = fileRoot.substring(fileRoot.lastIndexOf("/"), fileRoot.length()); File tmp = File.createTempFile(fileRoot, ".xml"); FileOutputStream fileOut = new FileOutputStream(tmp); BufferedOutputStream buffer = new BufferedOutputStream(fileOut); PrintStream out = new PrintStream(buffer); //Retrieve codebehind attribute from document info, if it is present. PyObject documentInfo = interp.get("__document__"); String codebehind = (String)documentInfo.__findattr__("codebehind").__tojava__(String.class); //Write beginning of XML document. out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); if (codebehind != null) { out.print("<ring codebehind=\"" + codebehind + "\">"); } else { out.println("<ring>"); } //Write out each persistable to the XML document. for (Persistable persistable : persistables) { String xml = persistable.toXML(); out.println(xml); } //Write end of XML document. out.println("</ring>"); XMLConverter.clear(); out.close(); buffer.close(); fileOut.close(); FileEntry fe = new FileEntry(); fe.setFile(tmp); fe.setEntryName("data" + fileRoot + ".xml"); //Prefix should start with a / entriesToAdd.add(fe); entriesToRemove.add(entry); //Cleanup! interp.cleanup(); } } //Modify the mudfile to remove the python data files and replace them with the //generated xml files. mudFile.getEntries().removeAll(entriesToRemove); mudFile.getEntries().addAll(entriesToAdd); } catch (IOException e) { e.printStackTrace(); } } private void validateDocuments() { for (FileEntry entry : mudFile.getEntries("data")) { try { //Only operate on XML documents. if (entry.getEntryName().endsWith(".xml")) { DocumentValidator dv = new DocumentValidator(); if (!dv.validate(entry.getFile())) { List<ValidationError> errors = dv.getErrors(); for (ValidationError error : errors) { error(entry.getEntryName(), error); } } } } catch (IOException e) { e.printStackTrace(); } } } private void validateIDs() { try { Set<String> duplicateChecker = new HashSet<String>(); for (FileEntry entry : mudFile.getEntries("data")) { if (entry.getEntryName().endsWith(".xml")) { IDFinder finder = new IDFinder(); XMLReader parser = XMLReaderFactory.createXMLReader(); parser.setContentHandler(finder); FileInputStream input = new FileInputStream(entry.getFile()); InputSource src = new InputSource(new BufferedInputStream(input)); parser.parse(src); for (String id : finder.getIDs()) { if (!duplicateChecker.add(id)) { error(entry.getEntryName(), "Duplicate object ID: " + id); } } } } } catch (IOException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } } /** * Generates an error message, but signifies that the mud compiler should continue * in order to possibly find more errors. * @param scope The place where the error occurred. Generally a filename. * @param message */ private void error(String scope, String message) { System.out.println(); System.out.println(scope + ": " + message); errorCount++; } /** * Overloaded error method that takes a ValidationError instead of a string for its * message. This special method displays line numbers. * @param scope * @param message */ private void error(String scope, ValidationError message) { System.out.println(); System.out.println(scope + "(" + message.getLine() + "):\n " + message.getError()); errorCount++; } /** * Generates an error message, and shuts down the JVM. A severe error means validation * cannot continue. * @param message */ private void severeError(String message) { System.out.println("Error (Severe): " + message); System.exit(1); } public void outputFile() { try { String dir = System.getProperty("user.dir"); //Generate the default filename, with all spaces stripped. String filename = mudFile.getName(); filename += "-" + mudFile.getVersion() + ".mud"; filename.replaceAll(" ", ""); FileOutputStream out = new FileOutputStream(dir + File.separator + filename); mudFile.writeTo(out); System.out.println("Wrote \"" + filename + "\""); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public MUDFile generateFile(String path) throws IOException { File dir = new File(path).getCanonicalFile(); stripIndex = dir.getPath().length() + File.separator.length(); MUDFile mudFile = new MUDFile(); File[] files = dir.listFiles(); for (File file : files) { file = file.getCanonicalFile(); if (file.isDirectory()) { List<FileEntry> entries = generateEntries(file); for (FileEntry entry : entries) { mudFile.addEntry(entry); } } } Properties props = new Properties(); props.load(new FileInputStream(dir.getPath() + "/mud.properties")); mudFile.setProperties(props); return mudFile; } public List<FileEntry> generateEntries(File dir) throws IOException { List<FileEntry> entries = new ArrayList<FileEntry>(); for (File file : dir.listFiles()) { file = file.getCanonicalFile(); if (file.isDirectory()) { entries.addAll(generateEntries(file)); } else { FileEntry entry = new FileEntry(file); String prefix = file.getPath().substring(stripIndex); entry.setEntryName(prefix); entries.add(entry); } } return entries; } }