| /******************************************************************************* |
| * Copyright (c) 2008-2018 Sonatype, Inc. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Sonatype, Inc. - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.m2e.tests.common; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertSame; |
| import static org.junit.Assert.fail; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.rules.TestName; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IMarker; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IProjectDescription; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspace; |
| import org.eclipse.core.resources.IWorkspaceDescription; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.IWorkspaceRunnable; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jdt.core.IClasspathContainer; |
| import org.eclipse.jdt.core.IClasspathEntry; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.jdt.core.JavaModelException; |
| |
| import org.codehaus.plexus.PlexusContainer; |
| import org.codehaus.plexus.component.repository.ComponentDescriptor; |
| import org.codehaus.plexus.util.IOUtil; |
| |
| import org.apache.maven.artifact.repository.ArtifactRepository; |
| import org.apache.maven.model.Model; |
| import org.apache.maven.wagon.Wagon; |
| |
| import org.eclipse.m2e.core.MavenPlugin; |
| import org.eclipse.m2e.core.embedder.IMavenConfiguration; |
| import org.eclipse.m2e.core.embedder.MavenModelManager; |
| import org.eclipse.m2e.core.internal.IMavenConstants; |
| import org.eclipse.m2e.core.internal.MavenPluginActivator; |
| import org.eclipse.m2e.core.internal.embedder.MavenImpl; |
| import org.eclipse.m2e.core.internal.lifecyclemapping.LifecycleMappingFactory; |
| import org.eclipse.m2e.core.internal.preferences.MavenConfigurationImpl; |
| import org.eclipse.m2e.core.internal.project.registry.MavenProjectFacade; |
| import org.eclipse.m2e.core.internal.project.registry.ProjectRegistryRefreshJob; |
| import org.eclipse.m2e.core.project.IMavenProjectFacade; |
| import org.eclipse.m2e.core.project.IMavenProjectImportResult; |
| import org.eclipse.m2e.core.project.IMavenProjectRegistry; |
| import org.eclipse.m2e.core.project.IProjectCreationListener; |
| import org.eclipse.m2e.core.project.MavenProjectChangedEvent; |
| import org.eclipse.m2e.core.project.MavenProjectInfo; |
| import org.eclipse.m2e.core.project.MavenUpdateRequest; |
| import org.eclipse.m2e.core.project.ProjectImportConfiguration; |
| import org.eclipse.m2e.core.project.ResolverConfiguration; |
| import org.eclipse.m2e.jdt.MavenJdtPlugin; |
| import org.eclipse.m2e.jdt.internal.BuildPathManager; |
| |
| |
| @SuppressWarnings("restriction") |
| public abstract class AbstractMavenProjectTestCase { |
| |
| public static final int DELETE_RETRY_COUNT = 10; |
| |
| public static final long DELETE_RETRY_DELAY = 6000L; |
| |
| protected static final IProgressMonitor monitor = new NullProgressMonitor(); |
| |
| protected IWorkspace workspace; |
| |
| @Rule |
| public TestName name = new TestName(); |
| |
| protected File repo; |
| |
| protected ProjectRegistryRefreshJob projectRefreshJob; |
| |
| protected Job downloadSourcesJob; |
| |
| protected IMavenConfiguration mavenConfiguration; |
| |
| private String oldUserSettingsFile; |
| |
| @Before |
| public void setUp() throws Exception { |
| workspace = ResourcesPlugin.getWorkspace(); |
| mavenConfiguration = MavenPlugin.getMavenConfiguration(); |
| setAutoBuilding(false); |
| setAutomaticallyUpdateConfiguration(false); |
| |
| // lets not assume we've got subversion in the target platform |
| Hashtable<String, String> options = JavaCore.getOptions(); |
| options.put(JavaCore.CORE_JAVA_BUILD_RESOURCE_COPY_FILTER, ".svn/"); |
| JavaCore.setOptions(options); |
| |
| projectRefreshJob = MavenPluginActivator.getDefault().getProjectManagerRefreshJob(); |
| projectRefreshJob.sleep(); |
| |
| downloadSourcesJob = ((BuildPathManager) MavenJdtPlugin.getDefault().getBuildpathManager()).getDownloadSourcesJob(); |
| downloadSourcesJob.sleep(); |
| |
| oldUserSettingsFile = mavenConfiguration.getUserSettingsFile(); |
| File settings = new File("settings.xml").getCanonicalFile(); |
| if(settings.canRead()) { |
| String userSettingsFile = settings.getAbsolutePath(); |
| mavenConfiguration.setUserSettingsFile(userSettingsFile); |
| } |
| |
| ArtifactRepository localRepository = MavenPlugin.getMaven().getLocalRepository(); |
| if(localRepository != null) { |
| repo = new File(localRepository.getBasedir()); |
| } else { |
| fail("Cannot determine local repository path"); |
| } |
| |
| // make sure all tests use default metadata by default |
| LifecycleMappingFactory.setUseDefaultLifecycleMappingMetadataSource(true); |
| LifecycleMappingFactory.setDefaultLifecycleMappingMetadataSource(null); |
| |
| WorkspaceHelpers.cleanWorkspace(); |
| FilexWagon.setRequestFailPattern(null); |
| FilexWagon.setRequestFilterPattern(null, true); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| waitForJobsToComplete(); |
| WorkspaceHelpers.cleanWorkspace(); |
| |
| // Restore the user settings file location |
| mavenConfiguration.setUserSettingsFile(oldUserSettingsFile); |
| |
| projectRefreshJob.wakeUp(); |
| setAutoBuilding(false); |
| setAutomaticallyUpdateConfiguration(false); |
| FilexWagon.reset(); |
| } |
| |
| /** |
| * @since 1.6.0 |
| */ |
| protected void setAutoBuilding(boolean autobuilding) throws CoreException { |
| IWorkspaceDescription description = workspace.getDescription(); |
| description.setAutoBuilding(autobuilding); |
| workspace.setDescription(description); |
| } |
| |
| /** |
| * @since 1.6.0 |
| */ |
| protected boolean isAutoBuilding() { |
| IWorkspaceDescription description = workspace.getDescription(); |
| return description.isAutoBuilding(); |
| } |
| |
| /** |
| * @since 1.6.0 |
| */ |
| protected void setAutomaticallyUpdateConfiguration(boolean update) { |
| ((MavenConfigurationImpl) mavenConfiguration).setAutomaticallyUpdateConfiguration(update); |
| } |
| |
| /** |
| * Synchronously refreshes maven project state. |
| * |
| * @see IMavenProjectRegistry#refresh(Collection, IProgressMonitor) |
| * @since 1.6.0 |
| */ |
| protected void refreshMavenProject(IProject project) { |
| MavenPlugin.getMavenProjectRegistry().refresh(new MavenUpdateRequest(project, false, false)); |
| } |
| |
| protected void deleteProject(String projectName) throws CoreException, InterruptedException { |
| IProject project = workspace.getRoot().getProject(projectName); |
| |
| deleteProject(project); |
| } |
| |
| protected void deleteProject(IProject project) throws InterruptedException, CoreException { |
| Exception cause = null; |
| for(int i = 0; i < DELETE_RETRY_COUNT; i++ ) { |
| try { |
| doDeleteProject(project); |
| } catch(InterruptedException e) { |
| throw e; |
| } catch(OperationCanceledException e) { |
| throw e; |
| } catch(Exception e) { |
| cause = e; |
| Thread.sleep(DELETE_RETRY_DELAY); |
| continue; |
| } |
| |
| // all clear |
| return; |
| } |
| |
| // must be a timeout |
| throw new CoreException(new Status(IStatus.ERROR, IMavenConstants.PLUGIN_ID, "Could not delete project", cause)); |
| } |
| |
| private void doDeleteProject(final IProject project) throws CoreException, InterruptedException { |
| waitForJobsToComplete(monitor); |
| |
| workspace.run(new IWorkspaceRunnable() { |
| public void run(IProgressMonitor monitor) throws CoreException { |
| if(project.exists()) { |
| deleteMember(".classpath", project, monitor); |
| deleteMember(".project", project, monitor); |
| project.delete(false, true, monitor); |
| } |
| } |
| |
| private void deleteMember(String name, final IProject project, IProgressMonitor monitor) throws CoreException { |
| IResource member = project.findMember(name); |
| if(member != null && member.exists()) { |
| member.delete(true, monitor); |
| } |
| } |
| }, new NullProgressMonitor()); |
| } |
| |
| protected IProject createProject(String projectName, final String pomResource) throws CoreException { |
| final IProject project = workspace.getRoot().getProject(projectName); |
| |
| workspace.run((IWorkspaceRunnable) monitor -> { |
| project.create(monitor); |
| |
| if(!project.isOpen()) { |
| project.open(monitor); |
| } |
| |
| IFile pomFile = project.getFile("pom.xml"); |
| if(!pomFile.exists()) { |
| InputStream is = null; |
| try { |
| is = new FileInputStream(pomResource); |
| pomFile.create(is, true, monitor); |
| } catch(FileNotFoundException ex) { |
| throw new CoreException(new Status(IStatus.ERROR, "", 0, ex.toString(), ex)); |
| } finally { |
| IOUtil.close(is); |
| } |
| } |
| }, null); |
| |
| return project; |
| } |
| |
| protected IProject createExisting(String projectName, String projectLocation) throws IOException, CoreException { |
| return createExisting(projectName, projectLocation, false); |
| } |
| |
| protected IProject createExisting(String projectName, String projectLocation, boolean addNature) |
| throws IOException, CoreException { |
| File dir = new File(workspace.getRoot().getLocation().toFile(), projectName); |
| if(dir.isFile()) { |
| dir = dir.getParentFile(); |
| } |
| copyDir(new File(projectLocation), dir); |
| |
| final IProject project = workspace.getRoot().getProject(projectName); |
| |
| workspace.run((IWorkspaceRunnable) monitor -> { |
| if(!project.exists()) { |
| IProjectDescription projectDescription = workspace.newProjectDescription(project.getName()); |
| if(addNature) { |
| projectDescription.setNatureIds(new String[] {IMavenConstants.NATURE_ID}); |
| } |
| projectDescription.setLocation(null); |
| project.create(projectDescription, monitor); |
| project.open(IResource.NONE, monitor); |
| } else { |
| project.refreshLocal(IResource.DEPTH_INFINITE, monitor); |
| } |
| }, null); |
| |
| // emulate behavior when autobuild was not honored by ProjectRegistryRefreshJob |
| if(!isAutoBuilding()) { |
| refreshMavenProject(project); |
| } |
| |
| return project; |
| } |
| |
| /** |
| * Import a test project into the Eclipse workspace |
| * |
| * @param pomLocation - a relative location of the pom file for the project to import |
| * @return created project |
| */ |
| protected IProject importProject(String pomLocation) throws IOException, CoreException { |
| return importProject(pomLocation, new ResolverConfiguration()); |
| } |
| |
| /** |
| * Import a test project into the Eclipse workspace |
| * |
| * @param pomLocation - a relative location of the pom file for the project to import |
| * @param configuration - a resolver configuration to be used to configure imported project |
| * @return created project |
| */ |
| protected IProject importProject(String pomLocation, ResolverConfiguration configuration) |
| throws IOException, CoreException { |
| return importProject(pomLocation, configuration, null); |
| } |
| |
| /** |
| * Import a test project into the Eclipse workspace |
| * |
| * @param pomLocation - a relative location of the pom file for the project to import |
| * @param configuration - a resolver configuration to be used to configure imported project |
| * @param listener - listener which will get notified of the raw project creation |
| * @return created project |
| */ |
| protected IProject importProject(String pomLocation, ResolverConfiguration configuration, |
| IProjectCreationListener listener) throws IOException, CoreException { |
| File pomFile = new File(pomLocation); |
| return importProjects(pomFile.getParentFile().getCanonicalPath(), new String[] {pomFile.getName()}, configuration, |
| false, listener)[0]; |
| } |
| |
| /** |
| * Import test projects into the Eclipse workspace |
| * |
| * @param basedir - a base directory for all projects to import |
| * @param pomNames - a relative locations of the pom files for the projects to import |
| * @param configuration - a resolver configuration to be used to configure imported projects |
| * @return created projects |
| */ |
| protected IProject[] importProjects(String basedir, String[] pomNames, ResolverConfiguration configuration) |
| throws IOException, CoreException { |
| return importProjects(basedir, pomNames, configuration, false); |
| } |
| |
| /** |
| * Import test projects into the Eclipse workspace |
| * |
| * @param basedir - a base directory for all projects to import |
| * @param pomNames - a relative locations of the pom files for the projects to import |
| * @param configuration - a resolver configuration to be used to configure imported projects |
| * @return created projects |
| */ |
| protected IProject[] importProjects(String basedir, String[] pomNames, ResolverConfiguration configuration, |
| boolean skipSanityCheck) throws IOException, CoreException { |
| return importProjects(basedir, pomNames, configuration, skipSanityCheck, null); |
| } |
| |
| protected IProject[] importProjects(String basedir, String[] pomNames, ResolverConfiguration configuration, |
| boolean skipSanityCheck, IProjectCreationListener listener) throws IOException, CoreException { |
| MavenModelManager mavenModelManager = MavenPlugin.getMavenModelManager(); |
| IWorkspaceRoot root = workspace.getRoot(); |
| |
| File src = new File(basedir); |
| File dst = new File(root.getLocation().toFile(), src.getName()); |
| copyDir(src, dst); |
| |
| final ArrayList<MavenProjectInfo> projectInfos = new ArrayList<MavenProjectInfo>(); |
| for(String pomName : pomNames) { |
| File pomFile = new File(dst, pomName); |
| Model model = mavenModelManager.readMavenModel(pomFile); |
| MavenProjectInfo projectInfo = new MavenProjectInfo(pomName, pomFile, model, null); |
| setBasedirRename(projectInfo); |
| projectInfos.add(projectInfo); |
| } |
| |
| final ProjectImportConfiguration importConfiguration = new ProjectImportConfiguration(configuration); |
| |
| final ArrayList<IMavenProjectImportResult> importResults = new ArrayList<IMavenProjectImportResult>(); |
| |
| workspace.run( |
| (IWorkspaceRunnable) monitor -> importResults.addAll(MavenPlugin.getProjectConfigurationManager() |
| .importProjects(projectInfos, importConfiguration, listener, monitor)), |
| MavenPlugin.getProjectConfigurationManager().getRule(), IWorkspace.AVOID_UPDATE, monitor); |
| |
| IProject[] projects = new IProject[projectInfos.size()]; |
| for(int i = 0; i < projectInfos.size(); i++ ) { |
| IMavenProjectImportResult importResult = importResults.get(i); |
| assertSame(projectInfos.get(i), importResult.getMavenProjectInfo()); |
| projects[i] = importResult.getProject(); |
| assertNotNull("Failed to import project " + projectInfos, projects[i]); |
| |
| /* |
| * Sanity check: make sure they were all imported |
| */ |
| if(!skipSanityCheck) { |
| Model model = projectInfos.get(i).getModel(); |
| IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().create(projects[i], monitor); |
| if(facade == null) { |
| fail("Project " + model.getGroupId() + "-" + model.getArtifactId() + "-" + model.getVersion() |
| + " was not imported. Errors: " |
| + WorkspaceHelpers.toString(WorkspaceHelpers.findErrorMarkers(projects[i]))); |
| } |
| } |
| } |
| |
| return projects; |
| } |
| |
| private void setBasedirRename(MavenProjectInfo projectInfo) throws IOException { |
| File workspaceRoot = workspace.getRoot().getLocation().toFile(); |
| File basedir = projectInfo.getPomFile().getParentFile().getCanonicalFile(); |
| |
| projectInfo.setBasedirRename( |
| basedir.getParentFile().equals(workspaceRoot) ? MavenProjectInfo.RENAME_REQUIRED : MavenProjectInfo.RENAME_NO); |
| } |
| |
| protected IProject importProject(String projectName, String projectLocation, ResolverConfiguration configuration) |
| throws IOException, CoreException { |
| ProjectImportConfiguration importConfiguration = new ProjectImportConfiguration(configuration); |
| importConfiguration.setProjectNameTemplate(projectName); |
| return importProject(projectName, projectLocation, importConfiguration); |
| } |
| |
| protected IProject importProject(String projectName, String projectLocation, |
| final ProjectImportConfiguration importConfiguration) throws IOException, CoreException { |
| File dir = new File(workspace.getRoot().getLocation().toFile(), projectName); |
| copyDir(new File(projectLocation), dir); |
| |
| File pomFile = new File(dir, IMavenConstants.POM_FILE_NAME); |
| Model model = MavenPlugin.getMavenModelManager().readMavenModel(pomFile); |
| final MavenProjectInfo projectInfo = new MavenProjectInfo(projectName, pomFile, model, null); |
| setBasedirRename(projectInfo); |
| |
| workspace.run((IWorkspaceRunnable) monitor -> { |
| MavenPlugin.getProjectConfigurationManager().importProjects(Collections.singleton(projectInfo), |
| importConfiguration, monitor); |
| IProject project = workspace.getRoot().getProject(importConfiguration.getProjectName(projectInfo.getModel())); |
| assertNotNull("Failed to import project " + projectInfo, project); |
| }, MavenPlugin.getProjectConfigurationManager().getRule(), IWorkspace.AVOID_UPDATE, monitor); |
| |
| return workspace.getRoot().getProject(projectName); |
| } |
| |
| protected void waitForJobsToComplete() throws InterruptedException, CoreException { |
| waitForJobsToComplete(monitor); |
| } |
| |
| public static void waitForJobsToComplete(IProgressMonitor monitor) throws InterruptedException, CoreException { |
| JobHelpers.waitForJobsToComplete(monitor); |
| } |
| |
| protected IClasspathEntry[] getMavenContainerEntries(IProject project) throws JavaModelException { |
| IJavaProject javaProject = JavaCore.create(project); |
| IClasspathContainer container = BuildPathManager.getMaven2ClasspathContainer(javaProject); |
| return container.getClasspathEntries(); |
| } |
| |
| protected static String toString(IMarker[] markers) { |
| return WorkspaceHelpers.toString(markers); |
| } |
| |
| protected static String toString(List<IMarker> markers) { |
| return WorkspaceHelpers.toString(markers); |
| } |
| |
| protected void copyContent(IProject project, String from, String to) throws Exception { |
| copyContent(project, project.getFile(from).getContents(), to, true); |
| } |
| |
| protected void copyContent(IProject project, String from, String to, boolean waitForJobsToComplete) throws Exception { |
| copyContent(project, project.getFile(from).getContents(), to, waitForJobsToComplete); |
| } |
| |
| protected void copyContent(IProject project, File from, String to) throws Exception { |
| copyContent(project, new FileInputStream(from), to, true); |
| } |
| |
| /** |
| * closes contents stream |
| */ |
| private void copyContent(IProject project, InputStream contents, String to, boolean waitForJobsToComplete) |
| throws CoreException, IOException, InterruptedException { |
| IFile file; |
| try { |
| file = project.getFile(to); |
| if(!file.exists()) { |
| file.create(contents, IResource.FORCE, monitor); |
| } else { |
| file.setContents(contents, IResource.FORCE, monitor); |
| } |
| } finally { |
| contents.close(); |
| } |
| if(waitForJobsToComplete) { |
| // emulate behavior when autobuild was not honored by ProjectRegistryRefreshJob |
| if(!isAutoBuilding() && file.getParent().getType() == IResource.PROJECT && file.getName().equals("pom.xml")) { |
| refreshMavenProject(project); |
| } |
| waitForJobsToComplete(); |
| } |
| } |
| |
| public static void copyDir(File src, File dst) throws IOException { |
| FileHelpers.copyDir(src, dst); |
| } |
| |
| public static void copyDir(File src, File dst, FileFilter filter) throws IOException { |
| FileHelpers.copyDir(src, dst, filter); |
| } |
| |
| protected static List<IMarker> findErrorMarkers(IProject project) throws CoreException { |
| return WorkspaceHelpers.findErrorMarkers(project); |
| } |
| |
| protected static List<IMarker> findMarkers(IProject project, int targetSeverity) throws CoreException { |
| return WorkspaceHelpers.findMarkers(project, targetSeverity); |
| } |
| |
| protected static void assertNoErrors(IProject project) throws CoreException { |
| WorkspaceHelpers.assertNoErrors(project); |
| } |
| |
| /** |
| * Returns a set of projects that were affected by specified collection of events |
| * |
| * @since 1.6.0 |
| */ |
| protected static Set<IProject> getProjectsFromEvents(Collection<MavenProjectChangedEvent> events) { |
| Set<IProject> projects = new HashSet<IProject>(); |
| for(MavenProjectChangedEvent event : events) { |
| projects.add(event.getSource().getProject()); |
| } |
| return projects; |
| } |
| |
| /** |
| * Assert that provided list <b>only</b> contains specified expected items. |
| * |
| * @since 1.6.0 |
| */ |
| @SafeVarargs |
| protected static <T> void assertContainsOnly(Set<? extends T> actual, T... expected) { |
| Set<T> expectedSet = new HashSet<T>(); |
| for(T item : expected) { |
| expectedSet.add(item); |
| } |
| assertEquals(expectedSet, actual); |
| } |
| |
| protected void injectFilexWagon() throws Exception { |
| PlexusContainer container = ((MavenImpl) MavenPlugin.getMaven()).getPlexusContainer(); |
| if(container.getContainerRealm().getResource(FilexWagon.class.getName().replace('.', '/') + ".class") == null) { |
| container.getContainerRealm().importFrom(FilexWagon.class.getClassLoader(), FilexWagon.class.getName()); |
| ComponentDescriptor<Wagon> descriptor = new ComponentDescriptor<Wagon>(); |
| descriptor.setRealm(container.getContainerRealm()); |
| descriptor.setRoleClass(Wagon.class); |
| descriptor.setImplementationClass(FilexWagon.class); |
| descriptor.setRoleHint("filex"); |
| descriptor.setInstantiationStrategy("per-lookup"); |
| container.addComponentDescriptor(descriptor); |
| } |
| } |
| |
| /** |
| * Nullifies all transient IMavenProjectFacade fields, which should have roughly the same effect as writing it to |
| * workspace state and reading it back after workspace restart. |
| */ |
| protected void deserializeFromWorkspaceState(final IMavenProjectFacade projectFacade) throws IllegalAccessException { |
| // pretend it was deserialized from workspace state |
| for(Field field : projectFacade.getClass().getDeclaredFields()) { |
| if(Modifier.isTransient(field.getModifiers())) { |
| field.setAccessible(true); |
| field.set(projectFacade, null); |
| } |
| } |
| MavenPluginActivator.getDefault().getMavenProjectManagerImpl().putMavenProject((MavenProjectFacade) projectFacade, |
| null); |
| } |
| |
| } |