--- a/java.j2seproject/nbproject/project.xml +++ a/java.j2seproject/nbproject/project.xml @@ -163,6 +163,14 @@ + org.netbeans.modules.java.preprocessorbridge + + + + 1.41 + + + org.netbeans.modules.java.project --- a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEActionProvider.java +++ a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEActionProvider.java @@ -48,7 +48,11 @@ import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.Reference; import java.lang.ref.WeakReference; +import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; @@ -57,14 +61,20 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.WeakHashMap; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; +import javax.swing.event.ChangeListener; import org.apache.tools.ant.module.api.support.ActionUtils; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.project.JavaProjectConstants; import org.netbeans.api.java.source.BuildArtifactMapper; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; import org.netbeans.modules.java.api.common.SourceRoots; import org.netbeans.modules.java.api.common.ant.UpdateHelper; import org.netbeans.modules.java.api.common.project.ProjectProperties; @@ -72,16 +82,24 @@ import org.netbeans.modules.java.api.common.project.BaseActionProvider.Callback3; import org.netbeans.modules.java.api.common.project.ProjectConfigurations; import org.netbeans.modules.java.j2seproject.api.J2SEBuildPropertiesProvider; +import org.netbeans.modules.java.preprocessorbridge.spi.CompileOnSaveAction; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.LookupProvider; import org.netbeans.spi.project.ProjectServiceProvider; import org.netbeans.spi.project.SingleMethod; import org.netbeans.spi.project.support.ant.PropertyEvaluator; +import org.openide.filesystems.FileLock; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; +import org.openide.modules.Places; +import org.openide.util.BaseUtilities; +import org.openide.util.ChangeSupport; +import org.openide.util.Exceptions; import org.openide.util.Lookup; +import org.openide.util.Pair; import org.openide.util.Parameters; import org.openide.util.WeakListeners; +import org.openide.util.lookup.ServiceProvider; /** Action provider of the J2SE project. This is the place where to do * strange things to J2SE actions. E.g. compile-single. @@ -341,8 +359,18 @@ } private static final class CosAction implements BuildArtifactMapper.ArtifactsUpdated, - PropertyChangeListener { + CompileOnSaveAction, PropertyChangeListener { + private static Map> instances = new WeakHashMap<>(); private static final String COS_UPDATED = "$cos.update"; //NOI18N + private static final String COS_CUSTOM = "$cos.update.custom"; //NOI18N + private static final String PROP_TARGET = "cos.update.target.internal"; //NOI18N + private static final String PROP_SCRIPT = "cos.update.script.internal"; //NOI18N + private static final String PROP_SRCDIR = "cos.src.dir.internal"; //NOI18N + private static final String PROP_INCLUDES ="cos.includes.internal"; //NOI18N + private static final String SNIPPETS = "executor-snippets"; //NOI18N + private static final String SCRIPT = "cos-update.xml"; //NOI18N + private static final String TARGET = "cos-update-internal"; //NOI18N + private static final String SCRIPT_TEMPLATE = "/org/netbeans/modules/java/j2seproject/resources/cos-update-snippet.xml"; //NOI18N private static final Object NONE = new Object(); private final J2SEActionProvider owner; private final PropertyEvaluator eval; @@ -350,7 +378,9 @@ private final SourceRoots tests; private final BuildArtifactMapper mapper; private final Map currentListeners; + private final ChangeSupport cs; private volatile Object targetCache; + private volatile Boolean enabledCache; private CosAction( @NonNull final J2SEActionProvider owner, @@ -363,32 +393,65 @@ this.tests = tests; this.mapper = new BuildArtifactMapper(); this.currentListeners = new HashMap<>(); + this.cs = new ChangeSupport(this); this.eval.addPropertyChangeListener(WeakListeners.propertyChange(this, this.eval)); this.src.addPropertyChangeListener(WeakListeners.propertyChange(this, this.src)); this.tests.addPropertyChangeListener(WeakListeners.propertyChange(this, this.tests)); updateRootsListeners(); + instances.put(owner.getProject(), new WeakReference<>(this)); } @Override + public boolean isEnabled() { + return getTarget() != null && isCustomUpdate(); + } + + @Override + public boolean isUpdateClasses() { + return isEnabled(); + } + + @Override + public boolean isUpdateResources() { + return isEnabled(); + } + + @Override + public Boolean performAction(Context ctx) throws IOException { + switch (ctx.getOperation()) { + case UPDATE: + return performUpdate(ctx); + case CLEAN: + return performClean(ctx); + case SYNC: + return performSync(ctx); + default: + throw new IllegalArgumentException(String.valueOf(ctx.getOperation())); + } + } + + @Override public void artifactsUpdated(@NonNull final Iterable artifacts) { - final String target = getTarget(); - if (target != null) { - final FileObject buildXml = owner.findBuildXml(); - if (buildXml != null) { - try { - ActionUtils.runTarget( - buildXml, - new String[] {target}, - null, - null); - } catch (IOException ioe) { - LOG.log( - Level.WARNING, - "Cannot execute pos compile on save target: {0} in: {1}", //NOI18N - new Object[]{ - target, - FileUtil.getFileDisplayName(buildXml) - }); + if (!isCustomUpdate()) { + final String target = getTarget(); + if (target != null) { + final FileObject buildXml = owner.findBuildXml(); + if (buildXml != null) { + try { + ActionUtils.runTarget( + buildXml, + new String[] {target}, + null, + null); + } catch (IOException ioe) { + LOG.log( + Level.WARNING, + "Cannot execute pos compile on save target: {0} in: {1}", //NOI18N + new Object[]{ + target, + FileUtil.getFileDisplayName(buildXml) + }); + } } } } @@ -397,13 +460,31 @@ @Override public void propertyChange(@NonNull final PropertyChangeEvent evt) { final String name = evt.getPropertyName(); - if (name == null || COS_UPDATED.equals(name)) { + if (name == null) { targetCache = null; - } else if (SourceRoots.PROP_ROOTS.equals(name)) { + enabledCache = null; + cs.fireChange(); + } else if (COS_UPDATED.equals(name)) { + targetCache = null; + cs.fireChange(); + } else if (COS_CUSTOM.equals(name)) { + enabledCache = null; + cs.fireChange(); + }else if (SourceRoots.PROP_ROOTS.equals(name)) { updateRootsListeners(); } } + @Override + public void addChangeListener(@NonNull final ChangeListener listener) { + cs.addChangeListener(listener); + } + + @Override + public void removeChangeListner(@NonNull final ChangeListener listener) { + cs.removeChangeListener(listener); + } + private void updateRootsListeners() { final Set newRoots = new HashSet<>(); Collections.addAll(newRoots, this.src.getRootURLs()); @@ -440,6 +521,123 @@ (String) target : null; } + + private boolean isCustomUpdate() { + Boolean res = enabledCache; + if (res == null) { + final String val = eval.getProperty(COS_CUSTOM); + res = enabledCache = Boolean.valueOf(val); + } + return res; + } + + @CheckForNull + private Boolean performUpdate(@NonNull final Context ctx) { + final String target = getTarget(); + if (target != null) { + final FileObject buildXml = owner.findBuildXml(); + if (buildXml != null) { + try { + final FileObject cosScript = getCosScript(); + final Iterable updated = ctx.getUpdated(); + final Iterable deleted = ctx.getDeleted(); + final File root = ctx.isCopyResources() ? + BaseUtilities.toFile(ctx.getSourceRoot().toURI()) : + ctx.getCacheRoot(); + final String includes = createIncludes(root, updated); + if (includes != null) { + final Properties props = new Properties(); + props.setProperty(PROP_TARGET, target); + props.setProperty(PROP_SCRIPT, FileUtil.toFile(buildXml).getAbsolutePath()); + props.setProperty(PROP_SRCDIR, root.getAbsolutePath()); + props.setProperty(PROP_INCLUDES, includes); + ActionUtils.runTarget( + cosScript, + new String[] {TARGET}, + props, + null); + } else { + LOG.warning("BuildArtifactMapper artifacts do not provide attributes."); //NOI18N + } + } catch (IOException | URISyntaxException e) { + LOG.log( + Level.WARNING, + "Cannot execute update targer on save target: {0} in: {1} due to: {2}", //NOI18N + new Object[]{ + target, + FileUtil.getFileDisplayName(buildXml), + e.getMessage() + }); + } + } + } + return true; + } + + @CheckForNull + private Boolean performClean(@NonNull final Context ctx) { + //Not sure what to do + return null; + } + + @CheckForNull + private Boolean performSync(@NonNull final Context ctx) { + //Not sure what to do + return null; + } + + @NonNull + private FileObject getCosScript() throws IOException { + final FileObject snippets = FileUtil.createFolder( + Places.getCacheSubdirectory(SNIPPETS)); + FileObject cosScript = snippets.getFileObject(SCRIPT); + if (cosScript == null) { + cosScript = FileUtil.createData(snippets, SCRIPT); + final FileLock lock = cosScript.lock(); + try (InputStream in = getClass().getResourceAsStream(SCRIPT_TEMPLATE); + OutputStream out = cosScript.getOutputStream(lock)) { + FileUtil.copy(in, out); + } finally { + lock.releaseLock(); + } + } + return cosScript; + } + + @CheckForNull + private static String createIncludes( + @NonNull final File root, + @NonNull final Iterable artifacts) { + final StringBuilder include = new StringBuilder(); + for (File f : artifacts) { + if (include.length() > 0) { + include.append(','); //NOI18N + } + include.append(relativize(f,root)); + } + return include.length() == 0 ? + null : + include.toString(); + } + + private static String relativize( + @NonNull final File file, + @NonNull final File folder) { + final String folderPath = folder.getAbsolutePath(); + int start = folderPath.length(); + if (!folderPath.endsWith(File.separator)) { + start++; + } + return file.getAbsolutePath().substring(start); + } + + @CheckForNull + static CosAction getInstance(@NonNull final Project p) { + final Reference r = instances.get(p); + return r != null ? + r.get() : + null; + } private static final class WeakArtifactUpdated extends WeakReference implements BuildArtifactMapper.ArtifactsUpdated, Runnable { @@ -473,4 +671,24 @@ } } } + + @ServiceProvider(service = CompileOnSaveAction.Provider.class, position = 10_000) + public static final class Provider implements CompileOnSaveAction.Provider { + + @Override + public CompileOnSaveAction forRoot(URL root) { + try { + final Project p = FileOwnerQuery.getOwner(root.toURI()); + if (p != null) { + p.getLookup().lookup(ActionProvider.class).getSupportedActions(); //Force initialization + final CosAction action = CosAction.getInstance(p); + return action; + } + } catch (URISyntaxException e) { + Exceptions.printStackTrace(e); + } + return null; + } + + } } --- a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/resources/cos-update-snippet.xml +++ a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/resources/cos-update-snippet.xml @@ -0,0 +1,51 @@ + + + + + + + + --- a/java.preprocessorbridge/nbproject/project.properties +++ a/java.preprocessorbridge/nbproject/project.properties @@ -37,7 +37,7 @@ # Contributor(s): is.autoload=true javac.compilerargs=-Xlint:unchecked -javac.source=1.7 -spec.version.base=1.40.0 +javac.source=1.8 +spec.version.base=1.41.0 javadoc.apichanges=${basedir}/apichanges.xml --- a/java.preprocessorbridge/nbproject/project.xml +++ a/java.preprocessorbridge/nbproject/project.xml @@ -54,6 +54,15 @@ + org.netbeans.api.java.classpath + + + + 1 + 1.52 + + + org.netbeans.libs.javacapi @@ -103,6 +112,33 @@ + + + unit + + org.netbeans.insane + + + + org.netbeans.libs.junit4 + + + + org.netbeans.modules.nbjunit + + + + org.openide.util + + + + + org.openide.util.lookup + + + + + com.ptc.rbinfohandler org.netbeans.modules.debugger.jpda.projects @@ -111,6 +147,7 @@ org.netbeans.modules.groovy.editor org.netbeans.modules.java.editor org.netbeans.modules.java.hints + org.netbeans.modules.java.j2seproject org.netbeans.modules.java.source org.netbeans.modules.java.source.base org.netbeans.modules.java.sourceui --- a/java.preprocessorbridge/src/org/netbeans/modules/java/preprocessorbridge/api/CompileOnSaveActionQuery.java +++ a/java.preprocessorbridge/src/org/netbeans/modules/java/preprocessorbridge/api/CompileOnSaveActionQuery.java @@ -0,0 +1,218 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.preprocessorbridge.api; + +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.util.Collection; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.modules.java.preprocessorbridge.spi.CompileOnSaveAction; +import org.openide.util.ChangeSupport; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.WeakListeners; + +/** + * Finds Compile On Save performer for given source root. + * @author Tomas Zezula + * @since 1.41 + */ +public final class CompileOnSaveActionQuery { + private static final Lookup.Result instances + = Lookup.getDefault().lookupResult(CompileOnSaveAction.Provider.class); + //Normalization Cache + //@GuardedBy("u2a") + private static final Map> u2a = new WeakHashMap<>(); + //@GuardedBy("u2a") + private static final Map a2u = new WeakHashMap<>(); + + private CompileOnSaveActionQuery() { + throw new IllegalStateException("No instance allowed."); //NOI18N + } + + /** + * Finds Compile On Save performer for given source root. + * @param sourceRoot the source root to find the performer for. + * @return the {@link CompileOnSaveAction} for performing compile on save or + * null in case when the root is not recognized. + */ + @CheckForNull + public static CompileOnSaveAction getAction(@NonNull final URL sourceRoot) { + CompileOnSaveAction res; + synchronized (u2a) { + final Reference ref = u2a.get(sourceRoot); + res = ref != null ? + ref.get() : + null; + } + if (res == null) { + final Collection actions = findAll(sourceRoot); + res = actions.isEmpty() ? + null : + new ProxyAction(sourceRoot, actions, instances); + synchronized (u2a) { + final Reference ref = u2a.get(sourceRoot); + CompileOnSaveAction tmpRes; + if (ref == null || (tmpRes = ref.get()) == null) { + u2a.put(sourceRoot, new WeakReference<>(res)); + a2u.put(res, sourceRoot); + } else { + res = tmpRes; + } + } + } + return res; + } + + private static Collection findAll(URL root) { + return instances.allInstances().stream() + .map((p) -> p.forRoot(root)) + .filter((a) -> a != null) + .collect(Collectors.toList()); + } + + private static final class ProxyAction implements CompileOnSaveAction, LookupListener, ChangeListener { + private static Predicate ALL = (a) -> true; + private static Predicate ACTIVE = (a) -> a.isEnabled(); + private final URL root; + private final AtomicReference> active; + private final ChangeSupport listeners; + + ProxyAction( + @NonNull final URL root, + @NonNull final Collection current, + @NonNull final Lookup.Result eventSource) { + this.root = root; + this.active = new AtomicReference<>(current); + this.listeners = new ChangeSupport(this); + instances.addLookupListener(WeakListeners.create( + LookupListener.class, + this, + instances)); + getActions(ALL) + .forEach((a) -> a.addChangeListener(WeakListeners.change(this, a))); + } + + @Override + public Boolean performAction(Context ctx) throws IOException { + return getActions(ACTIVE) + .findFirst() + .map((a) -> { + try { + return a.performAction(ctx); + } catch (IOException ioe) { + return null; + } + }) + .orElse(null); + } + + public boolean isEnabled() { + return getActions(ACTIVE) + .findAny() + .isPresent(); + } + + @Override + public boolean isUpdateResources() { + return getActions(ACTIVE) + .findFirst() + .map((a) -> a.isUpdateResources()) + .orElse(Boolean.FALSE); + } + + @Override + public boolean isUpdateClasses() { + return getActions(ACTIVE) + .findFirst() + .map((a) -> a.isUpdateClasses()) + .orElse(Boolean.FALSE); + } + + @Override + public void addChangeListener(@NonNull final ChangeListener l) { + this.listeners.addChangeListener(l); + } + + @Override + public void removeChangeListner(@NonNull final ChangeListener l) { + this.listeners.removeChangeListener(l); + } + + @Override + public void resultChanged(@NonNull final LookupEvent ev) { + reset(); + } + + @Override + public void stateChanged(ChangeEvent e) { + reset(); + } + + private void reset() { + this.active.set(null); + listeners.fireChange(); + } + + @NonNull + private Stream getActions(@NonNull final Predicate filter) { + Collection res = this.active.get(); + if (res == null) { + res = findAll(root); + this.active.compareAndSet(null, res); + } + return res.stream().filter(filter); + } + } +} --- a/java.preprocessorbridge/src/org/netbeans/modules/java/preprocessorbridge/spi/CompileOnSaveAction.java +++ a/java.preprocessorbridge/src/org/netbeans/modules/java/preprocessorbridge/spi/CompileOnSaveAction.java @@ -0,0 +1,380 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.preprocessorbridge.spi; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.event.ChangeListener; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.java.queries.BinaryForSourceQuery; +import org.openide.filesystems.FileUtil; +import org.openide.util.BaseUtilities; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.Parameters; + +/** + * The Compile On Save performer. + * @since 1.41 + * @author Tomas Zezula + */ +public interface CompileOnSaveAction { + + /** + * Performs the Compile On Save operation. + * @param ctx the context for Compile On Save operation + * @return true in case of success, false in case of failure, null in case of no changes. + * @throws IOException + */ + Boolean performAction (@NonNull final Context ctx) throws IOException; + /** + * Returns true when this action is enabled. + * The first enabled {@link CompileOnSaveAction} is used for performing the Compile On Save operation. + * @return true when enabled + */ + boolean isEnabled(); + /** + * Returns true when resources should be synchronized. + * @return true when resources should be copied + */ + boolean isUpdateResources(); + /** + * Returns true when classes should be synchronized. + * @return true when classes should be copied + */ + boolean isUpdateClasses(); + /** + * Adds {@link ChangeListener}. + * @param l the listener to be added + */ + void addChangeListener(@NonNull ChangeListener l); + /** + * Removes {@link ChangeListener}. + * @param l the listener to be removed + */ + void removeChangeListner(@NonNull ChangeListener l); + + /** + * Compile On Save operation. + */ + enum Operation { + /** + * Clean. + */ + CLEAN, + /** + * Partial update. + */ + UPDATE, + /** + * Full synchronization. + */ + SYNC + } + + /** + * Context of the Compile On Save Operation. + */ + final class Context { + private final Operation operation; + private final URL srcRoot; + private final boolean isCopyResources; + private final boolean isKeepResourcesUpToDate; + private final File cacheRoot; + private final Iterable updated; + private final Iterable deleted; + private final Object owner; + private final Consumer> firer; + + private Context( + @NonNull final Operation operation, + @NonNull final URL srcRoot, + final boolean isCopyResources, + final boolean isKeepResourcesUpToDate, + @NullAllowed final File cacheRoot, + @NullAllowed final Iterable updated, + @NullAllowed final Iterable deleted, + @NullAllowed final Object owner, + @NullAllowed final Consumer> firer) { + this.operation = operation; + this.srcRoot = srcRoot; + this.isCopyResources = isCopyResources; + this.isKeepResourcesUpToDate = isKeepResourcesUpToDate; + this.cacheRoot = cacheRoot; + this.updated = updated; + this.deleted = deleted; + this.owner = owner; + this.firer = firer; + } + + /** + * Returns the kind of the Compile On Save operation. + * @return the {@link Operation} + */ + @NonNull + public Operation getOperation() { + return operation; + } + + /** + * Returns the changed files. + * The operation is valid only for {@link Operation#UPDATE}. + * @return the changed files + */ + @NonNull + public Iterable getUpdated() { + if (operation != Operation.UPDATE) { + throw new IllegalStateException(); + } + return updated; + } + + /** + * Returns the deleted files. + * The operation is valid only for {@link Operation#UPDATE}. + * @return the deleted files + */ + @NonNull + public Iterable getDeleted() { + if (operation != Operation.UPDATE) { + throw new IllegalStateException(); + } + return deleted; + } + + /** + * Returns true for resources. + * The operation is valid only for {@link Operation#UPDATE} and {@link Operation#SYNC}. + * @return true for resources + */ + public boolean isCopyResources() { + if (operation == Operation.CLEAN) { + throw new IllegalStateException(); + } + return isCopyResources; + } + + /** + * Returns true if resources should be updated on change. + * The operation is valid only for {@link Operation#SYNC}. + * @return true for update on change + */ + public boolean isKeepResourcesUpToDate() { + if (operation != Operation.SYNC) { + throw new IllegalStateException(); + } + return isKeepResourcesUpToDate; + } + + /** + * Returns the source root. + * @return the source root + */ + @NonNull + public URL getSourceRoot() { + return srcRoot; + } + + /** + * Returns the cache root. + * The operation is valid only for {@link Operation#UPDATE}. + * @return the cache root. + */ + @NonNull + public File getCacheRoot() { + if (operation != Operation.UPDATE) { + throw new IllegalStateException(); + } + return cacheRoot; + } + + /** + * Returns the target folder. + * @return the target folder. + */ + @CheckForNull + public File getTarget() { + return getTarget(srcRoot); + } + + /** + * Returns the root owner. + * The operation is valid only for {@link Operation#SYNC}. + * @return the owner + */ + @NonNull + public Object getOwner() { + if (operation != Operation.SYNC) { + throw new IllegalStateException(); + } + return owner; + } + + /** + * Fires updated files. + * @param updatedFiles the updated files + */ + public void filesUpdated(@NonNull final Iterable updatedFiles) { + if (firer != null) { + firer.accept(updatedFiles); + } + } + + /** + * Creates context for clean operation. + * @param srcRoot the root + * @return the {@link Context} for clean operation + */ + @NonNull + public static Context clean(@NonNull final URL srcRoot) { + Parameters.notNull("srcRoot", srcRoot); //NOI18N + return new Context(Operation.CLEAN, srcRoot, false, false, null, null, null, null, null); + } + + /** + * Creates context for update operation. + * @param srcRoot the root + * @param isCopyResources true for resource update + * @param cacheRoot the cache root + * @param updated the changed files + * @param deleted the deleted files + * @param firer the fire callback + * @return the {@link Context} for update operation + */ + @NonNull + public static Context update( + @NonNull final URL srcRoot, + final boolean isCopyResources, + @NonNull final File cacheRoot, + @NonNull final Iterable updated, + @NonNull final Iterable deleted, + @NullAllowed final Consumer> firer) { + Parameters.notNull("srcRoot", srcRoot); //NOI18N + Parameters.notNull("cacheRoot", cacheRoot); //NOI18N + Parameters.notNull("updated", updated); //NOI18N + Parameters.notNull("deleted", deleted); //NOI18N + return new Context( + Operation.UPDATE, srcRoot, isCopyResources, false, cacheRoot, updated, deleted, null, firer); + } + + /** + * Creates context for sync operation. + * @param srcRoot the root + * @param isCopyResources should copy resources + * @param isKeepResourcesUpToDate should synchronize the resources on change + * @param owner the source root owner + * @return the {@link Context} for sync operation + */ + @NonNull + public static Context sync( + @NonNull final URL srcRoot, + final boolean isCopyResources, + final boolean isKeepResourcesUpToDate, + @NonNull final Object owner) { + Parameters.notNull("srcRoot", srcRoot); //NOI18N + Parameters.notNull("owner", owner); //NOI18N + return new Context( + Operation.SYNC, srcRoot, isCopyResources, isKeepResourcesUpToDate, null, null, null, owner, null); + } + + /** + * Returns the target folder for source root. + * @param srcRoot the source root to return target folder for + * @return the target folder + */ + @CheckForNull + public static File getTarget(@NonNull URL srcRoot) { + BinaryForSourceQuery.Result binaryRoots = BinaryForSourceQuery.findBinaryRoots(srcRoot); + + File result = null; + + for (URL u : binaryRoots.getRoots()) { + assert u != null : "Null in BinaryForSourceQuery.Result.roots: " + binaryRoots; //NOI18N + if (u == null) { + continue; + } + File f = FileUtil.archiveOrDirForURL(u); + + try { + if (FileUtil.isArchiveFile(BaseUtilities.toURI(f).toURL())) { + continue; + } + + if (f != null && result != null) { + Logger.getLogger(CompileOnSaveAction.class.getName()).log( + Level.WARNING, + "More than one binary directory for root: {0}", + srcRoot.toExternalForm()); + return null; + } + + result = f; + } catch (MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + } + + return result; + } + } + + /** + * The provider of the {@link CompileOnSaveAction}. + * The instances of the {@link Provider} should be registered in the + * global {@link Lookup}. + */ + interface Provider { + /** + * Finds the Compile On Save performer for given source root. + * @param root the root to find the Compile On Save performer for. + * @return the {@link CompileOnSaveAction} or null when the root is not recognized. + */ + CompileOnSaveAction forRoot(@NonNull final URL root); + } +} --- a/java.preprocessorbridge/test/unit/src/org/netbeans/modules/java/preprocessorbridge/api/CompileOnSaveActionQueryTest.java +++ a/java.preprocessorbridge/test/unit/src/org/netbeans/modules/java/preprocessorbridge/api/CompileOnSaveActionQueryTest.java @@ -0,0 +1,256 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.preprocessorbridge.api; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import javax.swing.event.ChangeListener; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.preprocessorbridge.spi.CompileOnSaveAction; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.BaseUtilities; +import org.openide.util.ChangeSupport; +import org.openide.util.Parameters; +import org.openide.util.test.MockChangeListener; +import org.openide.util.test.MockLookup; + +/** + * + * @author Tomas Zezula + */ +public class CompileOnSaveActionQueryTest extends NbTestCase { + + private URL nonSrcUrl; + private URL srcUrl1, srcUrl2; + private ActionImpl impl1, impl2, impl3; + + public CompileOnSaveActionQueryTest(@NonNull final String name) { + super(name); + } + + @Before + @Override + public void setUp() throws IOException { + clearWorkDir(); + final File wd = getWorkDir(); + final FileObject src1 = FileUtil.createFolder(FileUtil.normalizeFile( + new File(wd,"src"))); //NOI18N + final FileObject src2 = FileUtil.createFolder(FileUtil.normalizeFile( + new File(wd,"src2"))); //NOI18N + nonSrcUrl = BaseUtilities.toURI(wd).toURL(); + srcUrl1 = src1.toURL(); + srcUrl2 = src2.toURL(); + impl1 = new ActionImpl(); + impl2 = new ActionImpl(); + impl3 = new ActionImpl(); + MockLookup.setInstances( + new ProviderImpl(srcUrl1, impl1), + new ProviderImpl(srcUrl2, impl2), + new ProviderImpl(srcUrl1, impl3)); + //Enable all + impl1.setEnabled(true); + impl1.setUpdateClasses(true); + impl1.setUpdateResources(true); + impl2.setEnabled(true); + impl2.setUpdateClasses(true); + impl2.setUpdateResources(true); + impl3.setEnabled(true); + impl3.setUpdateClasses(true); + impl3.setUpdateResources(true); + } + + @After + @Override + public void tearDown() { + } + + public void testQuery() throws IOException { + CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(nonSrcUrl); + assertNull(a); + assertEquals(0, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(0, impl3.getInvocationCountAndReset()); + a = CompileOnSaveActionQuery.getAction(srcUrl1); + assertNotNull(a); + CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.clean(srcUrl1); + a.performAction(ctx); + assertEquals(1, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(0, impl3.getInvocationCountAndReset()); + a = CompileOnSaveActionQuery.getAction(srcUrl2); + assertNotNull(a); + ctx = CompileOnSaveAction.Context.clean(srcUrl2); + a.performAction(ctx); + assertEquals(0, impl1.getInvocationCountAndReset()); + assertEquals(1, impl2.getInvocationCountAndReset()); + assertEquals(0, impl3.getInvocationCountAndReset()); + } + + + public void testQueryChanges() throws IOException { + CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(srcUrl1); + assertNotNull(a); + CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.clean(srcUrl1); + a.performAction(ctx); + assertEquals(1, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(0, impl3.getInvocationCountAndReset()); + impl1.setEnabled(false); + a.performAction(ctx); + assertEquals(0, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(1, impl3.getInvocationCountAndReset()); + } + + + public void testQueryEvents() throws IOException { + CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(srcUrl1); + assertNotNull(a); + CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.clean(srcUrl1); + a.performAction(ctx); + assertEquals(1, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(0, impl3.getInvocationCountAndReset()); + final MockChangeListener l = new MockChangeListener(); + a.addChangeListener(l); + impl1.setEnabled(false); + l.assertEvent(); + a.performAction(ctx); + assertEquals(0, impl1.getInvocationCountAndReset()); + assertEquals(0, impl2.getInvocationCountAndReset()); + assertEquals(1, impl3.getInvocationCountAndReset()); + } + + + + private static final class ActionImpl implements CompileOnSaveAction { + + private final ChangeSupport listeners = new ChangeSupport(this); + private boolean enabled; + private boolean resEnabled; + private boolean clzEnabled; + private int invocationCount; + + @Override + public Boolean performAction(Context ctx) throws IOException { + invocationCount++; + return null; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean isUpdateResources() { + return resEnabled; + } + + @Override + public boolean isUpdateClasses() { + return clzEnabled; + } + + @Override + public void addChangeListener(ChangeListener l) { + listeners.addChangeListener(l); + } + + @Override + public void removeChangeListner(ChangeListener l) { + listeners.removeChangeListener(l); + } + + void setUpdateResources(final boolean v) { + this.resEnabled = v; + listeners.fireChange(); + } + + void setUpdateClasses(final boolean v) { + this.clzEnabled = v; + listeners.fireChange(); + } + + void setEnabled(final boolean v) { + this.enabled = v; + listeners.fireChange(); + } + + int getInvocationCountAndReset() { + int res = invocationCount; + invocationCount = 0; + return res; + } + } + + private static final class ProviderImpl implements CompileOnSaveAction.Provider { + private final URL root; + private final CompileOnSaveAction action; + + ProviderImpl( + @NonNull final URL root, + @NonNull final CompileOnSaveAction action) { + Parameters.notNull("root", root); //NOI18N + Parameters.notNull("action", action); //NOI18N + this.root = root; + this.action = action; + } + + @Override + @CheckForNull + public CompileOnSaveAction forRoot(@NonNull final URL root) { + if (this.root.equals(root)) { + return this.action; + } + return null; + } + } + + +} --- a/java.source.ant/antsrc/org/netbeans/modules/java/source/ant/CosUpdated.java +++ a/java.source.ant/antsrc/org/netbeans/modules/java/source/ant/CosUpdated.java @@ -0,0 +1,239 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2016 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2016 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.source.ant; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.Objects; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; +import org.apache.tools.ant.types.Reference; +import org.apache.tools.ant.types.Resource; + +/** + * + * @author Tomas Zezula + */ +public final class CosUpdated extends Task { + private String id; + private File srcdir; + private String includes; + + public void setId(final String id) { + id.getClass(); + this.id = id; + } + + + public String getId() { + return this.id; + } + + public void setSrcdir(final File dir) { + this.srcdir = dir; + } + + public File getSrcdir() { + return this.srcdir; + } + + public void setIncludes(final String includes) { + this.includes = includes; + } + + public String getIncludes() { + return this.includes; + } + + @Override + public void execute() throws BuildException { + if (this.id == null || this.id.isEmpty()) { + throw new BuildException("The id has to be set."); //NOI18N + } + if (this.srcdir == null || !this.srcdir.isDirectory()) { + throw new BuildException("The srcdir has to point to a directory."); //NOI18N + } + if (this.includes == null || this.includes.isEmpty()) { + throw new BuildException("The includes has to be set."); //NOI18N + } + final Project prj = getProject(); + final CosFileSet cfs = new CosFileSet(); + cfs.setProject(prj); + cfs.setDir(this.srcdir); + for (String include : includes.split(",")) { //NOI18N + include = include.trim(); + if (!include.isEmpty()) { + cfs.createInclude().setName(include); + } + } + prj.addReference(this.id, cfs); + } + + private static final class CosFileSet extends FileSet { + + @Override + public Iterator iterator() { + return new CosFileSetIterator(super.iterator()); + } + + @Override + public boolean isFilesystemOnly() { + return false; + } + + private static final class CosFileSetIterator implements Iterator { + + private final Iterator delegate; + + CosFileSetIterator(final Iterator delegate) { + delegate.getClass(); + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Resource next() { + return new CosResource(delegate.next()); + } + } + + private static final class CosResource extends Resource { + + private final Resource delegate; + + CosResource(final Resource delegate) { + delegate.getClass(); + this.delegate = delegate; + } + + @Override + public void setRefid(Reference r) { + throw tooManyAttributes(); + } + + @Override + public String getName() { + final String name = delegate.getName(); + return name.replace(".sig", ".class"); //NOI18N + } + + @Override + public boolean isExists() { + return delegate.isExists(); + } + + @Override + public long getLastModified() { + return delegate.getLastModified(); + } + + @Override + public boolean isDirectory() { + return delegate.isDirectory(); + } + + @Override + public long getSize() { + return delegate.getSize(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public int compareTo(Resource another) { + if (another instanceof CosResource) { + return delegate.compareTo(((CosResource)another).delegate); + } else { + return toString().compareTo(String.valueOf(another)); + } + } + + @Override + public int hashCode() { + int hash = 3; + hash = 71 * hash + Objects.hashCode(this.delegate); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return Objects.equals(this.delegate, ((CosResource)obj).delegate); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public boolean isFilesystemOnly() { + return false; + } + } + } +} --- a/java.source.ant/antsrc/org/netbeans/modules/java/source/ant/antlib.xml +++ a/java.source.ant/antsrc/org/netbeans/modules/java/source/ant/antlib.xml @@ -47,4 +47,5 @@ + --- a/java.source.base/nbproject/project.xml +++ a/java.source.base/nbproject/project.xml @@ -203,7 +203,7 @@ - 1.22 + 1.41 --- a/java.source.base/src/org/netbeans/modules/java/source/indexing/COSSynchronizingIndexer.java +++ a/java.source.base/src/org/netbeans/modules/java/source/indexing/COSSynchronizingIndexer.java @@ -54,6 +54,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.modules.java.preprocessorbridge.spi.CompileOnSaveAction; import org.netbeans.modules.java.source.usages.BuildArtifactMapperImpl; import org.netbeans.modules.parsing.impl.indexing.IndexerCache; import org.netbeans.modules.parsing.impl.indexing.IndexerCache.IndexerInfo; @@ -80,7 +81,7 @@ if (FileUtil.getArchiveFile(rootURL) != null) { return; } - if (!BuildArtifactMapperImpl.isUpdateResources(BuildArtifactMapperImpl.getTargetFolder(rootURL))) { + if (!BuildArtifactMapperImpl.isUpdateResources(rootURL)) { return ; } @@ -156,7 +157,11 @@ @Override public void filesDeleted(Iterable deleted, Context context) { - if (BuildArtifactMapperImpl.getTargetFolder(context.getRootURI()) == null) { + final File target = CompileOnSaveAction.Context.getTarget(context.getRootURI()); + if (target == null) { + return; + } + if (!BuildArtifactMapperImpl.isUpdateClasses(context.getRootURI())) { return ; } --- a/java.source.base/src/org/netbeans/modules/java/source/usages/BuildArtifactMapperImpl.java +++ a/java.source.base/src/org/netbeans/modules/java/source/usages/BuildArtifactMapperImpl.java @@ -68,17 +68,19 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.queries.AnnotationProcessingQuery; -import org.netbeans.api.java.queries.BinaryForSourceQuery; -import org.netbeans.api.java.queries.BinaryForSourceQuery.Result; import org.netbeans.api.java.queries.SourceForBinaryQuery; import org.netbeans.api.java.source.BuildArtifactMapper.ArtifactsUpdated; import org.netbeans.api.java.source.SourceUtils; import org.netbeans.api.queries.FileBuiltQuery; import org.netbeans.api.queries.FileBuiltQuery.Status; import org.netbeans.api.queries.VisibilityQuery; +import org.netbeans.modules.java.preprocessorbridge.api.CompileOnSaveActionQuery; +import org.netbeans.modules.java.preprocessorbridge.spi.CompileOnSaveAction; import org.netbeans.modules.java.source.indexing.COSSynchronizingIndexer; import org.netbeans.modules.java.source.indexing.JavaIndex; import org.netbeans.modules.java.source.parsing.FileObjects; @@ -90,7 +92,6 @@ import org.netbeans.modules.parsing.spi.indexing.ErrorsCache; import org.netbeans.spi.queries.FileBuiltQueryImplementation; import org.openide.filesystems.FileObject; -import org.openide.filesystems.FileStateInvalidException; import org.openide.filesystems.FileUtil; import org.openide.util.ChangeSupport; import org.openide.util.Exceptions; @@ -98,7 +99,9 @@ import org.openide.util.RequestProcessor; import org.openide.util.BaseUtilities; import org.openide.util.Lookup; +import org.openide.util.WeakListeners; import org.openide.util.WeakSet; +import org.openide.util.lookup.ServiceProvider; /** * @@ -175,229 +178,73 @@ } } - private static File getTarget(URL source) { - Result binaryRoots = BinaryForSourceQuery.findBinaryRoots(source); - - File result = null; - - for (URL u : binaryRoots.getRoots()) { - assert u != null : "Null in BinaryForSourceQuery.Result.roots: " + binaryRoots; //NOI18N - if (u == null) { - continue; - } - File f = FileUtil.archiveOrDirForURL(u); - - try { - if (FileUtil.isArchiveFile(BaseUtilities.toURI(f).toURL())) { - continue; - } - - if (f != null && result != null) { - Logger.getLogger(BuildArtifactMapperImpl.class.getName()).log(Level.WARNING, "More than one binary directory for root: {0}", source.toExternalForm()); - return null; - } - - result = f; - } catch (MalformedURLException ex) { - Exceptions.printStackTrace(ex); - } - } - - return result; - } - @SuppressWarnings("deprecation") public static Boolean ensureBuilt(URL sourceRoot, Object context, boolean copyResources, boolean keepResourceUpToDate) throws IOException { - File targetFolder = getTarget(sourceRoot); - - if (targetFolder == null) { - return null; + final CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(sourceRoot); + if (a != null) { + final CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.sync( + sourceRoot, + copyResources, + keepResourceUpToDate, + context); + return a.performAction(ctx); } - - try { - SourceUtils.waitScanFinished(); - } catch (InterruptedException e) { - //Not Important - LOG.log(Level.FINE, null, e); - return null; - } - - if (JavaIndex.ensureAttributeValue(sourceRoot, DIRTY_ROOT, null)) { - IndexingManager.getDefault().refreshIndexAndWait(sourceRoot, null); - } - - if (JavaIndex.getAttribute(sourceRoot, DIRTY_ROOT, null) != null) { - return false; - } - - FileObject[][] sources = new FileObject[1][]; - - if (!protectAgainstErrors(targetFolder, sources, context)) { - return false; - } - - File tagFile = new File(targetFolder, TAG_FILE_NAME); - File tagUpdateResourcesFile = new File(targetFolder, TAG_UPDATE_RESOURCES); - final boolean forceResourceCopy = copyResources && keepResourceUpToDate && !tagUpdateResourcesFile.exists(); - final boolean cosActive = tagFile.exists(); - if (cosActive && !forceResourceCopy) { - return true; - } - - if (!cosActive) { - delete(targetFolder, false/*#161085: cleanCompletely*/); - } - - if (!targetFolder.exists() && !targetFolder.mkdirs()) { - throw new IOException("Cannot create destination folder: " + targetFolder.getAbsolutePath()); - } - - sources(targetFolder, sources); - - for (int i = sources[0].length - 1; i>=0; i--) { - final FileObject sr = sources[0][i]; - if (!cosActive) { - URL srURL = sr.toURL(); - File index = JavaIndex.getClassFolder(srURL, true); - - if (index == null) { - //#181992: (not nice) ignore the annotation processing target directory: - if (srURL.equals(AnnotationProcessingQuery.getAnnotationProcessingOptions(sr).sourceOutputDirectory())) { - continue; - } - - return null; - } - - copyRecursively(index, targetFolder); - } - - if (copyResources) { - Set javaMimeTypes = COSSynchronizingIndexer.gatherJavaMimeTypes(); - String[] javaMimeTypesArr = javaMimeTypes.toArray(new String[0]); - - copyRecursively(sr, targetFolder, javaMimeTypes, javaMimeTypesArr); - } - } - - if (!cosActive) { - new FileOutputStream(tagFile).close(); - } - - if (keepResourceUpToDate) - new FileOutputStream(tagUpdateResourcesFile).close(); - - return true; + return null; } @SuppressWarnings("deprecation") public static Boolean clean(URL sourceRoot) throws IOException { - File targetFolder = getTarget(sourceRoot); - - if (targetFolder == null) { - return null; + final CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(sourceRoot); + if (a != null) { + final CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.clean(sourceRoot); + return a.performAction(ctx); } - - File tagFile = new File(targetFolder, TAG_FILE_NAME); - - if (!tagFile.exists()) { - return null; - } - - try { - SourceUtils.waitScanFinished(); - } catch (InterruptedException e) { - //Not Important - LOG.log(Level.FINE, null, e); - return false; - } - - delete(targetFolder, false); - delete(tagFile, true); - return null; } - public static File getTargetFolder(URL sourceRoot) { - File targetFolder = getTarget(sourceRoot); - - if (targetFolder == null) { - return null; - } - - if (!new File(targetFolder, TAG_FILE_NAME).exists()) { - return null; - } - - return targetFolder; + public static boolean isUpdateClasses(URL sourceRoot) { + final CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(sourceRoot); + return a != null ? + a.isUpdateClasses(): + false; } - public static boolean isUpdateResources(File targetFolder) { - return targetFolder != null && new File(targetFolder, TAG_UPDATE_RESOURCES).exists(); + public static boolean isUpdateResources(URL srcRoot) { + final CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(srcRoot); + return a != null ? + a.isUpdateResources(): + false; } public static void classCacheUpdated(URL sourceRoot, File cacheRoot, Iterable deleted, Iterable updated, boolean resource) { - if (!deleted.iterator().hasNext() && !updated.iterator().hasNext()) { - return ; - } - - File targetFolder = getTargetFolder(sourceRoot); - - if (targetFolder == null) { - return ; - } - - if (resource && !isUpdateResources(targetFolder)) { - return ; - } - - List updatedFiles = new LinkedList(); - - for (File deletedFile : deleted) { - final String relPath = relativizeFile(cacheRoot, deletedFile); - if (relPath == null) { - throw new IllegalArgumentException (String.format( - "Deleted file: %s is not under cache root: %s, (normalized file: %s).", //NOI18N - deletedFile.getAbsolutePath(), - cacheRoot.getAbsolutePath(), - FileUtil.normalizeFile(deletedFile).getAbsolutePath())); - } - File toDelete = resolveFile(targetFolder, relPath); - - toDelete.delete(); - updatedFiles.add(toDelete); - } - - for (File updatedFile : updated) { - final String relPath = relativizeFile(cacheRoot, updatedFile); - if (relPath == null) { - throw new IllegalArgumentException (String.format( - "Updated file: %s is not under cache root: %s, (normalized file: %s).", //NOI18N - updatedFile.getAbsolutePath(), - cacheRoot.getAbsolutePath(), - FileUtil.normalizeFile(updatedFile).getAbsolutePath())); - } - File target = resolveFile(targetFolder, relPath); - + final CompileOnSaveAction a = CompileOnSaveActionQuery.getAction(sourceRoot); + if (a != null) { try { - copyFile(updatedFile, target); - updatedFiles.add(target); + final CompileOnSaveAction.Context ctx = CompileOnSaveAction.Context.update( + sourceRoot, + resource, + cacheRoot, + updated, + deleted, + (updatedFiles) -> fire(sourceRoot, updatedFiles)); + a.performAction(ctx); } catch (IOException ex) { Exceptions.printStackTrace(ex); } } - - if (updatedFiles.size() > 0) { + } + + private static void fire( + @NonNull final URL sourceRoot, + @NonNull final Iterable updatedFiles) { + if (updatedFiles.iterator().hasNext()) { Set listeners; - synchronized (BuildArtifactMapperImpl.class) { listeners = source2Listener.get(sourceRoot); - if (listeners != null) { - listeners = new HashSet(listeners); + listeners = new HashSet<>(listeners); } } - if (listeners != null) { for (ArtifactsUpdated listener : listeners) { listener.artifactsUpdated(updatedFiles); @@ -405,7 +252,7 @@ } } } - + private static void copyFile(File updatedFile, File target) throws IOException { final File parent = target.getParentFile(); if (parent != null && !parent.exists()) { @@ -692,59 +539,44 @@ return delegate; } - File target = getTarget(owner.getURL()); - File tagFile = FileUtil.normalizeFile(new File(target, TAG_FILE_NAME)); + final CompileOnSaveAction action = CompileOnSaveActionQuery.getAction(owner.toURL()); + if (action == null) { + return delegate; + } synchronized(this) { - Reference ref = file2Listener.get(tagFile); - FileChangeListenerImpl l = ref != null ? ref.get() : null; - - if (l == null) { - file2Listener.put(tagFile, new WeakReference(l = new FileChangeListenerImpl())); - listener2File.put(l, tagFile); - FileChangeSupport.DEFAULT.addListener(l, tagFile); - } - Reference prevRef = file2Status.get(file); result = prevRef != null ? prevRef.get() : null; if (result == null) { - file2Status.put(file, new WeakReference(result = new FileBuiltQueryStatusImpl(delegate, tagFile, l))); + file2Status.put(file, new WeakReference(result = new FileBuiltQueryStatusImpl(delegate, action))); } return result; - } - } catch (FileStateInvalidException ex) { - Exceptions.printStackTrace(ex); - return null; + } } finally { recursive.remove(); } } } - - private static Map> file2Listener = new WeakHashMap>(); - private static Map listener2File = new WeakHashMap(); private static final class FileBuiltQueryStatusImpl implements FileBuiltQuery.Status, ChangeListener { private final FileBuiltQuery.Status delegate; - private final File tag; - private final FileChangeListenerImpl fileListener; + private final CompileOnSaveAction action; private final ChangeSupport cs = new ChangeSupport(this); - public FileBuiltQueryStatusImpl(Status delegate, File tag, FileChangeListenerImpl fileListener) { + public FileBuiltQueryStatusImpl(Status delegate, CompileOnSaveAction action) { this.delegate = delegate; - this.tag = tag; - this.fileListener = fileListener; + this.action = action; delegate.addChangeListener(this); - fileListener.addListener(this); + action.addChangeListener(WeakListeners.change(this, action)); } public boolean isBuilt() { - return delegate.isBuilt() || tag.canRead(); + return delegate.isBuilt() || action.isUpdateClasses(); } public void addChangeListener(ChangeListener l) { @@ -794,5 +626,287 @@ } }); } + } + + private static final class DefaultCompileOnSaveAction implements CompileOnSaveAction, ChangeListener { + //@GuardedBy("file2Listener") + private static Map> file2Listener = new WeakHashMap<>(); + //@GuardedBy("file2Listener") + private static Map listener2File = new WeakHashMap<>(); + + private final URL root; + private final ChangeSupport cs; + //@GuardedBy("file2Listener") + private FileChangeListenerImpl listenerDelegate; + + DefaultCompileOnSaveAction(@NonNull final URL root) { + this.root = root; + this.cs = new ChangeSupport(this); + } + + public boolean isEnabled() { + return true; + } + + @Override + public boolean isUpdateClasses() { + return isUpdateClasses(CompileOnSaveAction.Context.getTarget(root)); + } + + @Override + public boolean isUpdateResources() { + return isUpdateResources(CompileOnSaveAction.Context.getTarget(root)); + } + + @Override + public Boolean performAction(@NonNull final Context ctx) throws IOException { + assert root.equals(ctx.getSourceRoot()); + switch (ctx.getOperation()) { + case CLEAN: + return performClean(ctx); + case SYNC: + return performSync(ctx); + case UPDATE: + return performUpdate(ctx); + default: + } throw new IllegalArgumentException(String.valueOf(ctx.getOperation())); + } + + @Override + public void addChangeListener(@NonNull final ChangeListener listener) { + final File target = CompileOnSaveAction.Context.getTarget(root); + final File tagFile = FileUtil.normalizeFile(new File(target, TAG_FILE_NAME)); + synchronized (file2Listener) { + if (listenerDelegate == null) { + final Reference ref = file2Listener.get(tagFile); + FileChangeListenerImpl l = ref != null ? ref.get() : null; + if (l == null) { + file2Listener.put(tagFile, new WeakReference(l = new FileChangeListenerImpl())); + listener2File.put(l, tagFile); + FileChangeSupport.DEFAULT.addListener(l, tagFile); + //Need to hold l + } + listenerDelegate = l; + listenerDelegate.addListener(this); + } + } + cs.addChangeListener(listener); + } + + @Override + public void removeChangeListner(@NonNull final ChangeListener listener) { + cs.removeChangeListener(listener); + } + + @Override + public void stateChanged(@NonNull final ChangeEvent e) { + cs.fireChange(); + } + + private Boolean performClean(@NonNull final Context ctx) throws IOException { + final File targetFolder = ctx.getTarget(); + + if (targetFolder == null) { + return null; + } + + File tagFile = new File(targetFolder, TAG_FILE_NAME); + + if (!tagFile.exists()) { + return null; + } + + try { + SourceUtils.waitScanFinished(); + } catch (InterruptedException e) { + //Not Important + LOG.log(Level.FINE, null, e); + return false; + } + + delete(targetFolder, false); + delete(tagFile, true); + + return null; + } + + private Boolean performSync(@NonNull final Context ctx) throws IOException { + final URL sourceRoot = ctx.getSourceRoot(); + final File targetFolder = ctx.getTarget(); + final boolean copyResources = ctx.isCopyResources(); + final boolean keepResourceUpToDate = ctx.isKeepResourcesUpToDate(); + final Object context = ctx.getOwner(); + + if (targetFolder == null) { + return null; + } + + try { + SourceUtils.waitScanFinished(); + } catch (InterruptedException e) { + //Not Important + LOG.log(Level.FINE, null, e); + return null; + } + + if (JavaIndex.ensureAttributeValue(sourceRoot, DIRTY_ROOT, null)) { + IndexingManager.getDefault().refreshIndexAndWait(sourceRoot, null); + } + + if (JavaIndex.getAttribute(sourceRoot, DIRTY_ROOT, null) != null) { + return false; + } + + FileObject[][] sources = new FileObject[1][]; + + if (!protectAgainstErrors(targetFolder, sources, context)) { + return false; + } + + File tagFile = new File(targetFolder, TAG_FILE_NAME); + File tagUpdateResourcesFile = new File(targetFolder, TAG_UPDATE_RESOURCES); + final boolean forceResourceCopy = copyResources && keepResourceUpToDate && !tagUpdateResourcesFile.exists(); + final boolean cosActive = tagFile.exists(); + if (cosActive && !forceResourceCopy) { + return true; + } + + if (!cosActive) { + delete(targetFolder, false/*#161085: cleanCompletely*/); + } + + if (!targetFolder.exists() && !targetFolder.mkdirs()) { + throw new IOException("Cannot create destination folder: " + targetFolder.getAbsolutePath()); + } + + sources(targetFolder, sources); + + for (int i = sources[0].length - 1; i>=0; i--) { + final FileObject sr = sources[0][i]; + if (!cosActive) { + URL srURL = sr.toURL(); + File index = JavaIndex.getClassFolder(srURL, true); + + if (index == null) { + //#181992: (not nice) ignore the annotation processing target directory: + if (srURL.equals(AnnotationProcessingQuery.getAnnotationProcessingOptions(sr).sourceOutputDirectory())) { + continue; + } + + return null; + } + + copyRecursively(index, targetFolder); + } + + if (copyResources) { + Set javaMimeTypes = COSSynchronizingIndexer.gatherJavaMimeTypes(); + String[] javaMimeTypesArr = javaMimeTypes.toArray(new String[0]); + + copyRecursively(sr, targetFolder, javaMimeTypes, javaMimeTypesArr); + } + } + + if (!cosActive) { + new FileOutputStream(tagFile).close(); + } + + if (keepResourceUpToDate) + new FileOutputStream(tagUpdateResourcesFile).close(); + + return true; + } + + private Boolean performUpdate(@NonNull final Context ctx) throws IOException { + final Iterable deleted = ctx.getDeleted(); + final Iterable updated = ctx.getUpdated(); + final boolean resource = ctx.isCopyResources(); + final File cacheRoot = ctx.getCacheRoot(); + if (!deleted.iterator().hasNext() && !updated.iterator().hasNext()) { + return null; + } + File targetFolder = ctx.getTarget(); + if (targetFolder == null) { + return null; + } + if (!isUpdateClasses(targetFolder)) { + return null; + } + + if (resource && !isUpdateResources(targetFolder)) { + return null; + } + + List updatedFiles = new LinkedList<>(); + + for (File deletedFile : deleted) { + final String relPath = relativizeFile(cacheRoot, deletedFile); + if (relPath == null) { + throw new IllegalArgumentException (String.format( + "Deleted file: %s is not under cache root: %s, (normalized file: %s).", //NOI18N + deletedFile.getAbsolutePath(), + cacheRoot.getAbsolutePath(), + FileUtil.normalizeFile(deletedFile).getAbsolutePath())); + } + File toDelete = resolveFile(targetFolder, relPath); + + toDelete.delete(); + updatedFiles.add(toDelete); + } + + for (File updatedFile : updated) { + final String relPath = relativizeFile(cacheRoot, updatedFile); + if (relPath == null) { + throw new IllegalArgumentException (String.format( + "Updated file: %s is not under cache root: %s, (normalized file: %s).", //NOI18N + updatedFile.getAbsolutePath(), + cacheRoot.getAbsolutePath(), + FileUtil.normalizeFile(updatedFile).getAbsolutePath())); + } + File target = resolveFile(targetFolder, relPath); + + try { + copyFile(updatedFile, target); + updatedFiles.add(target); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + ctx.filesUpdated(updatedFiles); + return true; + } + + private boolean isUpdateClasses(@NullAllowed final File targetFolder) { + if (targetFolder == null) { + return false; + } + return new File(targetFolder, TAG_FILE_NAME).exists(); + } + + private boolean isUpdateResources(@NullAllowed final File targetFolder) { + if (targetFolder == null) { + return false; + } + return new File(targetFolder, TAG_UPDATE_RESOURCES).exists(); + } + } + + @ServiceProvider(service = CompileOnSaveAction.Provider.class, position = Integer.MAX_VALUE) + public static final class Provider implements CompileOnSaveAction.Provider { + //@GuardedBy("normCache") + private final Map> normCache + = new WeakHashMap<>(); + @Override + public CompileOnSaveAction forRoot(@NonNull final URL root) { + synchronized (normCache) { + final Reference ref = normCache.get(root); + DefaultCompileOnSaveAction res; + if (ref == null || (res = ref.get()) == null) { + res = new DefaultCompileOnSaveAction(root); + normCache.put(root, new WeakReference<>(res)); + } + return res; + } + } } }