/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2014 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 3 for more details. * * You should have received a copy of the GNU General Public License version 3 * along with this work; if not, see http://www.gnu.org/licenses/ * * * Please visit http://neilcsmith.net if you need additional information or * have any questions. */ package net.neilcsmith.praxis.live.pxj; import java.io.BufferedReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.nio.CharBuffer; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.neilcsmith.praxis.compiler.ClassBodyContext; import net.neilcsmith.praxis.core.info.ArgumentInfo; import org.netbeans.api.actions.Openable; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionReferences; import org.openide.cookies.OpenCookie; import org.openide.filesystems.FileChangeAdapter; import org.openide.filesystems.FileEvent; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileSystem; import org.openide.filesystems.FileUtil; import org.openide.filesystems.MIMEResolver; import org.openide.loaders.DataObject; import org.openide.loaders.DataObjectExistsException; import org.openide.loaders.MultiDataObject; import org.openide.loaders.MultiFileLoader; import org.openide.util.Exceptions; import org.openide.util.NbBundle.Messages; @Messages({ "LBL_PXJ_LOADER=Files of PXJ" }) @MIMEResolver.ExtensionRegistration( displayName = "#LBL_PXJ_LOADER", mimeType = "text/x-praxis-java", extension = {"pxj"} ) @DataObject.Registration( mimeType = "text/x-praxis-java", iconBase = "net/neilcsmith/praxis/live/pxj/resources/pxj16.png", displayName = "#LBL_PXJ_LOADER", position = 300 ) @ActionReferences({ @ActionReference( path = "Loaders/text/x-praxis-java/Actions", id = @ActionID(category = "System", id = "org.openide.actions.OpenAction"), position = 100, separatorAfter = 200 ), @ActionReference( path = "Loaders/text/x-praxis-java/Actions", id = @ActionID(category = "Edit", id = "org.openide.actions.DeleteAction"), position = 600 ), /*@ActionReference( path="Loaders/text/x-praxis-java/Actions", id=@ActionID(category="System", id="org.openide.actions.RenameAction"), position=700, separatorAfter=800 ),*/}) public class PXJDataObject extends MultiDataObject { private final static Logger LOG = Logger.getLogger(PXJDataObject.class.getName()); final static String PXJ_DOB_KEY = "PXJ_DOB"; private final FileObject pxjFile; private final ClassBodyContext<?> classBodyContext; private FileObject javaProxy; private String defaultImports; private String classDeclaration; private String classEnding; public PXJDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException { super(pf, loader); this.pxjFile = pf; classBodyContext = findClassBodyContext(pf); getCookieSet().add(new Open()); } private ClassBodyContext<?> findClassBodyContext(FileObject f) { try { Object o = f.getAttribute("argumentInfo"); if (o instanceof ArgumentInfo) { o = ((ArgumentInfo) o).getProperties().get(ClassBodyContext.KEY); if (o != null) { Class<?> cls = Class.forName(o.toString(), true, Thread.currentThread().getContextClassLoader()); if (cls != null && ClassBodyContext.class.isAssignableFrom(cls)) { o = cls.newInstance(); return (ClassBodyContext<?>) o; } } } } catch (Exception ex) { LOG.log(Level.WARNING, "", ex); } return null; } @Override protected int associateLookup() { return 1; } private void openProxy() { try { if (javaProxy == null) { javaProxy = constructProxy(); } DataObject dob = DataObject.find(javaProxy); Openable openable = dob.getLookup().lookup(Openable.class); if (openable != null) { openable.open(); } } catch (Exception ex) { } } @Override protected void dispose() { super.dispose(); disposeProxy(); } void disposeProxy() { if (javaProxy != null) { try { javaProxy.delete(); javaProxy = null; } catch (IOException ex) { Exceptions.printStackTrace(ex); } } } private void refreshDataFromProxy() { try { if (javaProxy == null) { return; } String data = javaProxy.asText(); if (defaultImports != null && !defaultImports.isEmpty()) { data = data.replace(defaultImports, ""); } if (classDeclaration != null) { data = data.replace(classDeclaration, ""); } // if (classEnding != null) { // data = data.replace(classEnding, ""); // } int lastFold = data.lastIndexOf("}"); if (lastFold > 0) { data = data.substring(0, lastFold); } try (OutputStreamWriter writer = new OutputStreamWriter(pxjFile.getOutputStream())) { writer.append(data); } } catch (IOException ex) { Exceptions.printStackTrace(ex); } } private FileObject constructProxy() throws Exception { FileSystem fs = FileUtil.createMemoryFileSystem(); FileObject f = fs.getRoot().createData(pxjFile.getName(), "java"); OutputStreamWriter writer = null; try { writer = new OutputStreamWriter(f.getOutputStream()); writer.append(constructProxyContent()); } finally { if (writer != null) { writer.close(); } } f.setAttribute(PXJ_DOB_KEY, this); f.addFileChangeListener(new ProxyListener()); return f; } private String constructProxyContent() { try { String fileContents = pxjFile.asText(); BufferedReader r = new BufferedReader(new StringReader(fileContents)); ClassBodyContext<?> context = classBodyContext; if (context == null) { // what now? } String[] extraImports = parseImportDeclarations(r); StringBuilder sb = new StringBuilder(); sb.append(getDefaultImports(context)); for (String extraImp : extraImports) { sb.append("import ").append(extraImp).append(";\n"); } sb.append("\n"); sb.append(getClassDeclaration(context)); sb.append("\n"); String line; boolean top = true; while ((line = r.readLine()) != null) { if (top) { if ( line.trim().isEmpty()) { continue; } top = false; } sb.append(line).append('\n'); } if (sb.charAt(sb.length() - 1) != '\n') { sb.append('\n'); } sb.append(getClassEnding(context)); return sb.toString(); } catch (IOException ex) { return ""; } } private String getDefaultImports(ClassBodyContext<?> context) { if (defaultImports == null) { String[] importList = context.getDefaultImports(); if (importList.length > 0) { StringBuilder sb = new StringBuilder(); sb.append("//<editor-fold defaultstate=\"collapsed\" desc=\"Default Imports\">"); sb.append("//GEN-BEGIN:imports"); for (String imp : importList) { sb.append("\nimport "); sb.append(imp); sb.append(";"); } sb.append("//</editor-fold>//GEN-END:imports\n"); defaultImports = sb.toString(); } else { defaultImports = ""; } } return defaultImports; } private String getClassDeclaration(ClassBodyContext<?> context) { if (classDeclaration == null) { StringBuilder sb = new StringBuilder(); sb.append("//<editor-fold defaultstate=\"collapsed\" desc=\"Class Declaration\">"); sb.append("//GEN-BEGIN:classdec\n"); sb.append("class "); sb.append(pxjFile.getName()); sb.append(" extends "); sb.append(context.getExtendedClass().getName()); // @TODO support interfaces sb.append(" {\n"); sb.append("//</editor-fold>//GEN-END:classdec\n"); classDeclaration = sb.toString(); } return classDeclaration; } private String getClassEnding(ClassBodyContext<?> context) { if (classEnding == null) { // StringBuilder sb = new StringBuilder(); // sb.append("//<editor-fold defaultstate=\"collapsed\" desc=\"Class Ending\">"); // sb.append("//GEN-BEGIN:classend\n"); // sb.append("}\n"); // sb.append("//</editor-fold>//GEN-END:classend\n"); // classEnding = sb.toString(); classEnding = "}//GEN-BEGIN:classend\n//GEN-END:classend"; } return classEnding; } /** * Copied from Janino ClassBodyEvaluator * * Heuristically parse IMPORT declarations at the beginning of the character stream produced * by the given {@link Reader}. After this method returns, all characters up to and including * that last IMPORT declaration have been read from the {@link Reader}. * <p> * This method does not handle comments and string literals correctly, i.e. if a pattern that * looks like an IMPORT declaration appears within a comment or a string literal, it will be * taken as an IMPORT declaration. * * @param r A {@link Reader} that supports MARK, e.g. a {@link BufferedReader} * @return The parsed imports, e.g. {@code { "java.util.*", "static java.util.Map.Entry" }} */ private static String[] parseImportDeclarations(Reader r) throws IOException { final CharBuffer cb = CharBuffer.allocate(10000); r.mark(cb.limit()); r.read(cb); cb.rewind(); List<String> imports = new ArrayList<String>(); int afterLastImport = 0; for (Matcher matcher = IMPORT_STATEMENT_PATTERN.matcher(cb); matcher.find();) { imports.add(matcher.group(1)); afterLastImport = matcher.end(); } r.reset(); r.skip(afterLastImport); return imports.toArray(new String[imports.size()]); } private static final Pattern IMPORT_STATEMENT_PATTERN = Pattern.compile( "\\bimport\\s+" + "(" + "(?:static\\s+)?" + "[\\p{javaLowerCase}\\p{javaUpperCase}_\\$][\\p{javaLowerCase}\\p{javaUpperCase}\\d_\\$]*" + "(?:\\.[\\p{javaLowerCase}\\p{javaUpperCase}_\\$][\\p{javaLowerCase}\\p{javaUpperCase}\\d_\\$]*)*" + "(?:\\.\\*)?" + ");" ); private class Open implements OpenCookie { @Override public void open() { openProxy(); } } private class ProxyListener extends FileChangeAdapter { @Override public void fileChanged(FileEvent fe) { if (fe.getFile() == javaProxy) { refreshDataFromProxy(); } else { fe.getFile().removeFileChangeListener(this); } } } }