/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.sling.ide.eclipse.core.internal; import static org.apache.sling.ide.artifacts.EmbeddedArtifactLocator.SUPPORT_BUNDLE_SYMBOLIC_NAME; import static org.apache.sling.ide.artifacts.EmbeddedArtifactLocator.SUPPORT_SOURCE_BUNDLE_SYMBOLIC_NAME; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.sling.ide.artifacts.EmbeddedArtifact; import org.apache.sling.ide.artifacts.EmbeddedArtifactLocator; import org.apache.sling.ide.eclipse.core.ISlingLaunchpadServer; import org.apache.sling.ide.eclipse.core.ServerUtil; import org.apache.sling.ide.log.Logger; import org.apache.sling.ide.osgi.OsgiClient; import org.apache.sling.ide.osgi.OsgiClientException; import org.apache.sling.ide.serialization.SerializationException; import org.apache.sling.ide.transport.Batcher; import org.apache.sling.ide.transport.Command; import org.apache.sling.ide.transport.Repository; import org.apache.sling.ide.transport.RepositoryInfo; import org.apache.sling.ide.transport.ResourceProxy; import org.apache.sling.ide.transport.Result; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.wst.server.core.IModule; import org.eclipse.wst.server.core.IServer; import org.eclipse.wst.server.core.model.IModuleResource; import org.eclipse.wst.server.core.model.IModuleResourceDelta; import org.eclipse.wst.server.core.model.ServerBehaviourDelegate; import org.osgi.framework.Version; public class SlingLaunchpadBehaviour extends ServerBehaviourDelegateWithModulePublishSupport { private ResourceChangeCommandFactory commandFactory; private ILaunch launch; private JVMDebuggerConnection debuggerConnection; @Override public void stop(boolean force) { if (debuggerConnection!=null) { debuggerConnection.stop(force); } setServerState(IServer.STATE_STOPPED); try { ServerUtil.stopRepository(getServer(), new NullProgressMonitor()); } catch (CoreException e) { Activator.getDefault().getPluginLogger().warn("Failed to stop repository", e); } } public void start(IProgressMonitor monitor) throws CoreException { boolean success = false; Result<ResourceProxy> result = null; monitor = SubMonitor.convert(monitor, "Starting server", 10).setWorkRemaining(50); Repository repository; RepositoryInfo repositoryInfo; OsgiClient client; try { repository = ServerUtil.connectRepository(getServer(), monitor); repositoryInfo = ServerUtil.getRepositoryInfo(getServer(), monitor); client = Activator.getDefault().getOsgiClientFactory().createOsgiClient(repositoryInfo); } catch (CoreException e) { setServerState(IServer.STATE_STOPPED); throw e; } catch (URISyntaxException e) { setServerState(IServer.STATE_STOPPED); throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); } monitor.worked(10); // 10/50 done try { EmbeddedArtifactLocator artifactLocator = Activator.getDefault().getArtifactLocator(); installBundle(monitor,client, artifactLocator.loadSourceSupportBundle(), SUPPORT_SOURCE_BUNDLE_SYMBOLIC_NAME); // 15/50 done installBundle(monitor,client, artifactLocator.loadToolingSupportBundle(), SUPPORT_BUNDLE_SYMBOLIC_NAME); // 20/50 done } catch ( IOException | OsgiClientException e) { Activator.getDefault().getPluginLogger() .warn("Failed reading the installation support bundle", e); } try { if (getServer().getMode().equals(ILaunchManager.DEBUG_MODE)) { debuggerConnection = new JVMDebuggerConnection(client); success = debuggerConnection.connectInDebugMode(launch, getServer(), SubMonitor.convert(monitor, 30)); // 50/50 done } else { Command<ResourceProxy> command = repository.newListChildrenNodeCommand("/"); result = command.execute(); success = result.isSuccess(); monitor.worked(30); // 50/50 done } if (success) { setServerState(IServer.STATE_STARTED); } else { setServerState(IServer.STATE_STOPPED); String message = "Unable to connect to the Server. Please make sure a server instance is running "; if (result != null) { message += " (" + result.toString() + ")"; } throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, message)); } } catch ( CoreException | RuntimeException e ) { setServerState(IServer.STATE_STOPPED); throw e; } finally { monitor.done(); } } private void installBundle(IProgressMonitor monitor, OsgiClient client, final EmbeddedArtifact bundle, String bundleSymbolicName) throws OsgiClientException, IOException { Version embeddedVersion = new Version(bundle.getOsgiFriendlyVersion()); monitor.setTaskName("Installing " + bundleSymbolicName + " " + embeddedVersion); Version remoteVersion = client.getBundleVersion(bundleSymbolicName); monitor.worked(2); ISlingLaunchpadServer launchpadServer = (ISlingLaunchpadServer) getServer().loadAdapter(SlingLaunchpadServer.class, monitor); if (remoteVersion == null || remoteVersion.compareTo(embeddedVersion) < 0 || ( remoteVersion.equals(embeddedVersion) || "SNAPSHOT".equals(embeddedVersion.getQualifier()))) { try ( InputStream contents = bundle.openInputStream() ){ client.installBundle(contents, bundle.getName()); } remoteVersion = embeddedVersion; } launchpadServer.setBundleVersion(bundleSymbolicName, remoteVersion, monitor); monitor.worked(3); } // TODO refine signature public void setupLaunch(ILaunch launch, String launchMode, IProgressMonitor monitor) throws CoreException { // TODO check that ports are free this.launch = launch; setServerRestartState(false); setServerState(IServer.STATE_STARTING); setMode(launchMode); } @Override protected void publishModule(int kind, int deltaKind, IModule[] module, IProgressMonitor monitor) throws CoreException { Logger logger = Activator.getDefault().getPluginLogger(); if (commandFactory == null) { commandFactory = new ResourceChangeCommandFactory(Activator.getDefault().getSerializationManager(), Activator.getDefault().getPreferences().getIgnoredFileNamesForSync()); } logger.trace(traceOperation(kind, deltaKind, module)); if (getServer().getServerState() == IServer.STATE_STOPPED) { logger.trace("Ignoring request to publish module when the server is stopped"); setModulePublishState(module, IServer.PUBLISH_STATE_NONE); return; } if ((kind == IServer.PUBLISH_AUTO || kind == IServer.PUBLISH_INCREMENTAL) && deltaKind == ServerBehaviourDelegate.NO_CHANGE) { logger.trace("Ignoring request to publish the module when no resources have changed; most likely another module has changed"); setModulePublishState(module, IServer.PUBLISH_STATE_NONE); return; } if (kind == IServer.PUBLISH_FULL && deltaKind == ServerBehaviourDelegate.REMOVED) { logger.trace("Ignoring request to unpublish all of the module resources"); setModulePublishState(module, IServer.PUBLISH_STATE_NONE); return; } try { if (ProjectHelper.isBundleProject(module[0].getProject())) { String serverMode = getServer().getMode(); if (!serverMode.equals(ILaunchManager.DEBUG_MODE) || kind==IServer.PUBLISH_CLEAN) { // in debug mode, we rely on the hotcode replacement feature of eclipse/jvm // otherwise, for run and profile modes we explicitly publish the bundle module // TODO: make this configurable as part of the server config // SLING-3655 : when doing PUBLISH_CLEAN, the bundle deployment mechanism should // still be triggered publishBundleModule(module, monitor); } } else if (ProjectHelper.isContentProject(module[0].getProject())) { try { publishContentModule(kind, deltaKind, module, monitor); } catch (SerializationException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Serialization error for " + traceOperation(kind, deltaKind, module).toString(), e)); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IO error for " + traceOperation(kind, deltaKind, module).toString(), e)); } } } catch (CoreException e) { // in case of errors always require full redeployment of the whole module setModulePublishState(module, IServer.PUBLISH_STATE_FULL); throw e; } } private String traceOperation(int kind, int deltaKind, IModule[] module) { StringBuilder trace = new StringBuilder(); trace.append("SlingLaunchpadBehaviour.publishModule("); switch (kind) { case IServer.PUBLISH_CLEAN: trace.append("PUBLISH_CLEAN, "); break; case IServer.PUBLISH_INCREMENTAL: trace.append("PUBLISH_INCREMENTAL, "); break; case IServer.PUBLISH_AUTO: trace.append("PUBLISH_AUTO, "); break; case IServer.PUBLISH_FULL: trace.append("PUBLISH_FULL, "); break; default: trace.append("UNKNOWN - ").append(kind).append(", "); } switch (deltaKind) { case ServerBehaviourDelegate.ADDED: trace.append("ADDED, "); break; case ServerBehaviourDelegate.CHANGED: trace.append("CHANGED, "); break; case ServerBehaviourDelegate.NO_CHANGE: trace.append("NO_CHANGE, "); break; case ServerBehaviourDelegate.REMOVED: trace.append("REMOVED, "); break; default: trace.append("UNKONWN - ").append(deltaKind).append(", "); break; } switch (getServer().getServerState()) { case IServer.STATE_STARTED: trace.append("STARTED, "); break; case IServer.STATE_STARTING: trace.append("STARTING, "); break; case IServer.STATE_STOPPED: trace.append("STOPPED, "); break; case IServer.STATE_STOPPING: trace.append("STOPPING, "); break; default: trace.append("UNKONWN - ").append(getServer().getServerState()).append(", "); break; } trace.append(Arrays.toString(module)).append(")"); return trace.toString(); } private void publishBundleModule(IModule[] module, IProgressMonitor monitor) throws CoreException { final IProject project = module[0].getProject(); boolean installLocally = getServer().getAttribute(ISlingLaunchpadServer.PROP_INSTALL_LOCALLY, true); monitor.beginTask("deploying via local install", 5); try { OsgiClient osgiClient = Activator.getDefault().getOsgiClientFactory() .createOsgiClient(ServerUtil.getRepositoryInfo(getServer(), monitor)); Version supportBundleVersion = osgiClient .getBundleVersion(EmbeddedArtifactLocator.SUPPORT_BUNDLE_SYMBOLIC_NAME); monitor.worked(1); if (supportBundleVersion == null) { throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "The support bundle was not found, please install it via the server properties page.")); } IJavaProject javaProject = ProjectHelper.asJavaProject(project); IFolder outputFolder = (IFolder) project.getWorkspace().getRoot().findMember(javaProject.getOutputLocation()); IPath outputLocation = outputFolder.getLocation(); //ensure the MANIFEST.MF exists - if it doesn't then let the publish fail with a warn (instead of an error) IResource manifest = outputFolder.findMember("META-INF/MANIFEST.MF"); if (manifest==null) { Activator.getDefault().getPluginLogger().warn("Project "+project+" does not have a META-INF/MANIFEST.MF (yet) - not publishing this time"); Activator.getDefault().issueConsoleLog("InstallBundle", outputFolder.getLocation().toOSString(), "Project "+project+" does not have a META-INF/MANIFEST.MF (yet) - not publishing this time"); monitor.done(); setModulePublishState(module, IServer.PUBLISH_STATE_FULL); return; } monitor.worked(1); //TODO SLING-3767: //osgiClient must have a timeout!!! if ( installLocally ) { osgiClient.installLocalBundle(outputLocation.toOSString()); monitor.worked(3); } else { JarBuilder builder = new JarBuilder(); InputStream bundle = builder.buildJar(outputFolder); monitor.worked(1); osgiClient.installLocalBundle(bundle, outputFolder.getLocation().toOSString()); monitor.worked(2); } setModulePublishState(module, IServer.PUBLISH_STATE_NONE); } catch (URISyntaxException e1) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e1.getMessage(), e1)); } catch (OsgiClientException e1) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed installing bundle : " + e1.getMessage(), e1)); } finally { monitor.done(); } } private void publishContentModule(int kind, int deltaKind, IModule[] module, IProgressMonitor monitor) throws CoreException, SerializationException, IOException { Logger logger = Activator.getDefault().getPluginLogger(); Repository repository = ServerUtil.getConnectedRepository(getServer(), monitor); if (repository == null) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to find a repository for server " + getServer())); } Batcher batcher = Activator.getDefault().getBatcherFactory().createBatcher(); // TODO it would be more efficient to have a module -> filter mapping // it would be simpler to implement this in SlingContentModuleAdapter, but // the behaviour for resources being filtered out is deletion, and that // would be an incorrect ( or at least suprising ) behaviour at development time List<IModuleResource> addedOrUpdatedResources = new ArrayList<>(); IModuleResource[] allResources = getResources(module); Set<IPath> handledPaths = new HashSet<>(); switch (deltaKind) { case ServerBehaviourDelegate.CHANGED: for (IModuleResourceDelta resourceDelta : getPublishedResourceDelta(module)) { StringBuilder deltaTrace = new StringBuilder(); deltaTrace.append("- processing delta kind "); switch (resourceDelta.getKind()) { case IModuleResourceDelta.ADDED: deltaTrace.append("ADDED "); break; case IModuleResourceDelta.CHANGED: deltaTrace.append("CHANGED "); break; case IModuleResourceDelta.NO_CHANGE: deltaTrace.append("NO_CHANGE "); break; case IModuleResourceDelta.REMOVED: deltaTrace.append("REMOVED "); break; default: deltaTrace.append("UNKNOWN - ").append(resourceDelta.getKind()); } deltaTrace.append("for resource ").append(resourceDelta.getModuleResource()); logger.trace(deltaTrace.toString()); switch (resourceDelta.getKind()) { case IModuleResourceDelta.ADDED: case IModuleResourceDelta.CHANGED: case IModuleResourceDelta.NO_CHANGE: // TODO is this needed? Command<?> command = addFileCommand(repository, resourceDelta.getModuleResource()); if (command != null) { ensureParentIsPublished(resourceDelta.getModuleResource(), repository, allResources, handledPaths, batcher); addedOrUpdatedResources.add(resourceDelta.getModuleResource()); } enqueue(batcher, command); break; case IModuleResourceDelta.REMOVED: enqueue(batcher, removeFileCommand(repository, resourceDelta.getModuleResource())); break; } } break; case ServerBehaviourDelegate.ADDED: case ServerBehaviourDelegate.NO_CHANGE: // TODO is this correct ? for (IModuleResource resource : getResources(module)) { Command<?> command = addFileCommand(repository, resource); enqueue(batcher, command); if (command != null) { addedOrUpdatedResources.add(resource); } } break; case ServerBehaviourDelegate.REMOVED: for (IModuleResource resource : getResources(module)) { enqueue(batcher, removeFileCommand(repository, resource)); } break; } // reorder the child nodes at the end, when all create/update/deletes have been processed for (IModuleResource resource : addedOrUpdatedResources) { enqueue(batcher, reorderChildNodesCommand(repository, resource)); } execute(batcher); // set state to published super.publishModule(kind, deltaKind, module, monitor); setModulePublishState(module, IServer.PUBLISH_STATE_NONE); // setServerPublishState(IServer.PUBLISH_STATE_NONE); } private void execute(Batcher batcher) throws CoreException { for ( Command<?> command : batcher.get()) { Result<?> result = command.execute(); if (!result.isSuccess()) { // TODO - proper error logging throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed publishing path=" + command.getPath() + ", result=" + result.toString())); } } } /** * Ensures that the parent of this resource has been published to the repository * * <p> * Note that the parents explicitly do not have their child nodes reordered, this will happen when they are * published due to a resource change * </p> * * @param moduleResource the current resource * @param repository the repository to publish to * @param allResources all of the module's resources * @param handledPaths the paths that have been handled already in this publish operation, but possibly not * registered as published * @param batcher * @throws IOException * @throws SerializationException * @throws CoreException */ private void ensureParentIsPublished(IModuleResource moduleResource, Repository repository, IModuleResource[] allResources, Set<IPath> handledPaths, Batcher batcher) throws CoreException, SerializationException, IOException { Logger logger = Activator.getDefault().getPluginLogger(); IPath currentPath = moduleResource.getModuleRelativePath(); logger.trace("Ensuring that parent of path {0} is published", currentPath); // we assume the root is always published if (currentPath.segmentCount() == 0) { logger.trace("Path {0} can not have a parent, skipping", currentPath); return; } IPath parentPath = currentPath.removeLastSegments(1); // already published by us, a parent of another resource that was published in this execution if (handledPaths.contains(parentPath)) { logger.trace("Parent path {0} was already handled, skipping", parentPath); return; } for (IModuleResource maybeParent : allResources) { if (maybeParent.getModuleRelativePath().equals(parentPath)) { // handle the parent's parent first, if needed ensureParentIsPublished(maybeParent, repository, allResources, handledPaths, batcher); // create this resource enqueue(batcher, addFileCommand(repository, maybeParent)); handledPaths.add(maybeParent.getModuleRelativePath()); logger.trace("Ensured that resource at path {0} is published", parentPath); return; } } throw new IllegalArgumentException("Resource at " + moduleResource.getModuleRelativePath() + " has parent path " + parentPath + " but no resource with that path is in the module's resources."); } private void enqueue(Batcher batcher, Command<?> command) { if (command == null) { return; } batcher.add(command); } private Command<?> addFileCommand(Repository repository, IModuleResource resource) throws CoreException, SerializationException, IOException { IResource res = getResource(resource); if (res == null) { return null; } return commandFactory.newCommandForAddedOrUpdated(repository, res); } private Command<?> reorderChildNodesCommand(Repository repository, IModuleResource resource) throws CoreException, SerializationException, IOException { IResource res = getResource(resource); if (res == null) { return null; } return commandFactory.newReorderChildNodesCommand(repository, res); } private IResource getResource(IModuleResource resource) { IResource file = (IFile) resource.getAdapter(IFile.class); if (file == null) { file = (IFolder) resource.getAdapter(IFolder.class); } if (file == null) { // Usually happens on server startup, it seems to be safe to ignore for now Activator.getDefault().getPluginLogger() .trace("Got null {0} and {1} for {2}", IFile.class.getSimpleName(), IFolder.class.getSimpleName(), resource); return null; } return file; } private Command<?> removeFileCommand(Repository repository, IModuleResource resource) throws SerializationException, IOException, CoreException { IResource deletedResource = getResource(resource); if (deletedResource == null) { return null; } return commandFactory.newCommandForRemovedResources(repository, deletedResource); } }