| /* |
| * 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.EclipseResources; |
| 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.sync.content.SyncCommandFactory; |
| 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 SyncCommandFactory 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 = Activator.getDefault().getCommandFactory(); |
| } |
| |
| 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.newCommandForAddedOrUpdatedResource(repository, EclipseResources.create(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, EclipseResources.create(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.newCommandForRemovedResource(repository, EclipseResources.create(deletedResource)); |
| } |
| } |