blob: 97181a54ad3f7f7a4ead580b725ebeaf62c6ad6d [file] [log] [blame]
/**
* 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.netbeans.modules.apisupport.project.ui.wizard.common;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.Tree;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import javax.lang.model.element.TypeElement;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.GeneratorUtilities;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.modules.apisupport.project.api.ManifestManager;
import org.netbeans.modules.apisupport.project.api.Util;
import org.netbeans.modules.apisupport.project.api.EditableManifest;
import org.netbeans.modules.apisupport.project.api.LayerHandle;
import org.netbeans.modules.apisupport.project.spi.LayerUtil;
import org.netbeans.modules.apisupport.project.spi.NbModuleProvider;
import org.netbeans.modules.apisupport.project.spi.NbModuleProvider.ModuleDependency;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.support.ant.PropertyUtils;
import org.openide.ErrorManager;
import org.openide.cookies.SaveCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.CreateFromTemplateAttributesProvider;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.modules.SpecificationVersion;
import org.openide.util.Lookup;
import org.openide.util.Parameters;
import org.openide.util.lookup.ServiceProvider;
/**
* Provide general infrastructure for performing miscellaneous operations upon
* {@link NbModuleProject}'s files, such as <em>manifest.mf</em>,
* <em>bundle.properties</em>, <em>manifest.xml</em>, <em>project.xml</em> easily.
* See javadoc to individual methods below. After creating a
* <code>CreatedModifiedFiles</code> instance client may create {@link
* Operation} which then may be added to the
* <code>CreatedModifiedFiles</code> instance or just used itself. Both
* <code>CreatedModifiedFiles</code> and
* <code>Operation</code> provide methods to get sets of relative (to a
* project's base directory) paths which are going to be created and/or
* modified. These sets may be obtained
* <strong>before</strong> added operation are run so they can be e.g. shown by
* wizard before any files are actually created.
*
* @author Martin Krauskopf
*/
public final class CreatedModifiedFiles {
/**
* Operation that may be added to a
* <code>CreatedModifiedFiles</code> instance or can just be used alone. See
* {@link CreatedModifiedFiles} for more information.
*/
public interface Operation {
/**
* Perform this operation.
*/
void run() throws IOException;
/**
* Returns sorted array of path which are going to modified after this
* {@link CreatedModifiedFiles} instance is run. Paths are relative to
* the project's base directory. It is available immediately after an
* operation instance is created. XXX why is this sorted, and not a
* simple Set<String>?
*/
String[] getModifiedPaths();
/**
* Returns sorted array of path which are going to created after this
* {@link CreatedModifiedFiles} instance is run. Paths are relative to
* the project's base directory. It is available immediately after an
* operation instance is created.
*/
String[] getCreatedPaths();
/**
* returns paths that are already existing but the operaton expects to
* create it. Is an error condition and should be shown in UI.
*
*/
String[] getInvalidPaths();
/* XXX should perhaps also have:
/**
* True if the created or modified path is relevant to the user and should
* be selected in the final wizard.
* /
boolean isRelevant(String path);
/**
* True if the created or modified path should be opened in the editor.
* /
boolean isForEditing(String path);
*/
}
public static abstract class AbstractOperation implements Operation {
private Project project;
private SortedSet<String> createdPaths;
private SortedSet<String> modifiedPaths;
private SortedSet<String> invalidPaths;
protected AbstractOperation(Project project) {
this.project = project;
}
protected Project getProject() {
return project;
}
protected NbModuleProvider getModuleInfo() {
return getProject().getLookup().lookup(NbModuleProvider.class);
}
@Override
public String[] getModifiedPaths() {
String[] s = new String[getModifiedPathsSet().size()];
return getModifiedPathsSet().toArray(s);
}
@Override
public String[] getCreatedPaths() {
String[] s = new String[getCreatedPathsSet().size()];
return getCreatedPathsSet().toArray(s);
}
@Override
public String[] getInvalidPaths() {
String[] s = new String[getInvalidPathsSet().size()];
return getInvalidPathsSet().toArray(s);
}
protected void addCreatedOrModifiedPath(String relPath, boolean allowFileModification) {
// XXX this is probably wrong, since it might be created by an earlier op:
if (getProject().getProjectDirectory().getFileObject(relPath) == null) {
getCreatedPathsSet().add(relPath);
} else {
if (allowFileModification) {
getModifiedPathsSet().add(relPath);
} else {
getInvalidPathsSet().add(relPath);
}
}
}
protected void addPaths(Operation o) {
getCreatedPathsSet().addAll(Arrays.asList(o.getCreatedPaths()));
getModifiedPathsSet().addAll(Arrays.asList(o.getModifiedPaths()));
getInvalidPathsSet().addAll(Arrays.asList(o.getInvalidPaths()));
}
protected SortedSet<String> getCreatedPathsSet() {
if (createdPaths == null) {
createdPaths = new TreeSet<String>();
}
return createdPaths;
}
protected SortedSet<String> getInvalidPathsSet() {
if (invalidPaths == null) {
invalidPaths = new TreeSet<String>();
}
return invalidPaths;
}
protected SortedSet<String> getModifiedPathsSet() {
if (modifiedPaths == null) {
modifiedPaths = new TreeSet<String>();
}
return modifiedPaths;
}
protected boolean addCreatedFileObject(FileObject fo) {
Parameters.notNull("fo", fo);
return getCreatedPathsSet().add(getProjectPath(fo));
}
protected boolean addModifiedFileObject(FileObject fo) {
Parameters.notNull("fo", fo);
return getModifiedPathsSet().add(getProjectPath(fo));
}
/**
* Doesn't check given arguments. Be sure they are valid as supposed by
* {@link PropertyUtils#relativizeFile(File, File)} method.
*/
private String getProjectPath(FileObject file) {
return PropertyUtils.relativizeFile(
FileUtil.toFile(getProject().getProjectDirectory()),
FileUtil.normalizeFile(FileUtil.toFile(file)));
}
}
private final SortedSet<String> createdPaths = new TreeSet<String>();
private final SortedSet<String> modifiedPaths = new TreeSet<String>();
private final SortedSet<String> invalidPaths = new TreeSet<String>();
/**
* {@link Project} this instance manage.
*/
private final Project project;
private final List<Operation> operations = new ArrayList<Operation>();
// For use from LayerModifications; XXX would be better to have an operation context or similar
// (so that multiple operations could group pre- and post-actions)
private LayerHandle layerHandle;
LayerHandle getLayerHandle() {
if (layerHandle == null) {
layerHandle = LayerHandle.forProject(project);
}
return layerHandle;
}
/**
* Create instance for managing given {@link NbModuleProject}'s files.
*
* @param project project this instance will operate upon
*/
public CreatedModifiedFiles(Project project) {
this.project = project;
}
/**
* Adds given {@link Operation} to a list of operations that will be run
* after calling {@link #run()}. Operations are run in the order in which
* they have been added. Also files which would be created by a given
* operation are added to lists of paths returned by {@link
* #getModifiedPaths()} or {@link #getCreatedPaths()} immediately.
*
* @param operation operation to be added
*/
public void add(Operation operation) {
operations.add(operation);
// XXX should always show isForEditing files at the top of the list, acc. to Jano
createdPaths.addAll(Arrays.asList(operation.getCreatedPaths()));
modifiedPaths.addAll(Arrays.asList(operation.getModifiedPaths()));
invalidPaths.addAll(Arrays.asList(operation.getInvalidPaths()));
}
/**
* Performs in turn {@link Operation#run()} on all operations added to this
* instance in order in which operations have been added.
*/
public void run() throws IOException {
boolean oldAutosave = false;
if (layerHandle != null) {
oldAutosave = layerHandle.isAutosave();
layerHandle.setAutosave(false);
}
try {
//aggregate all Add Dependency operations into a single operation..
AddModuleDependency depOp = null;
Iterator<Operation> it = operations.iterator();
while (it.hasNext()) {
Operation oper = it.next();
if (oper instanceof AddModuleDependency) {
if (depOp == null) {
depOp = (AddModuleDependency) oper;
} else {
depOp.addDependencies(((AddModuleDependency) oper).getDependencies());
it.remove();
}
}
}
//aggregate all Add Module To Target Platform operations into a single operation..
AddModuleToTargetPlatform targetPlatfOp = null;
Iterator<Operation> it2 = operations.iterator();
while (it2.hasNext()) {
Operation oper = it2.next();
if (oper instanceof AddModuleToTargetPlatform) {
if (targetPlatfOp == null) {
targetPlatfOp = (AddModuleToTargetPlatform) oper;
} else {
targetPlatfOp.addDependencies(((AddModuleToTargetPlatform) oper).getDependencies());
it2.remove();
}
}
}
//and now execute
for (Operation op : operations) {
op.run();
}
if (layerHandle != null) {
// XXX clumsy, see above
layerHandle.save();
}
} finally {
if (layerHandle != null) {
layerHandle.setAutosave(oldAutosave);
}
}
// XXX should get EditCookie/OpenCookie for created/modified files for which isForEditing
// XXX should return a Set<FileObject> of created/modified files for which isRelevant
}
public String[] getCreatedPaths() {
if (createdPaths == null) {
return new String[0];
} else {
String[] s = new String[createdPaths.size()];
return createdPaths.toArray(s);
}
}
public String[] getModifiedPaths() {
if (modifiedPaths == null) {
return new String[0];
} else {
String[] s = new String[modifiedPaths.size()];
return modifiedPaths.toArray(s);
}
}
public String[] getInvalidPaths() {
if (invalidPaths == null) {
return new String[0];
} else {
String[] s = new String[invalidPaths.size()];
return invalidPaths.toArray(s);
}
}
/**
* Convenience method to load a file template from the standard location.
*
* @param name a simple filename
* @return that file from * the <code>Templates/NetBeansModuleDevelopment-files</code> manifest
* folder
*/
public static FileObject getTemplate(String name) {
FileObject f = FileUtil.getConfigFile("Templates/NetBeansModuleDevelopment-files/" + name);
assert f != null : name;
return f;
}
/**
* Returns {@link Operation} for creating custom file in the project file
* hierarchy.
*
* @param path relative to a project directory where a file to be created
* @param content content for the file being created. Content may address
* either text or binary data.
*/
public Operation createFile(String path, FileObject content) {
return new CreateFile(project, path, content);
}
/**
* Returns an {@link Operation} for creating custom file in the project file
* hierarchy with an option to replace <em>token</em>s from a given
* <code>content</code> with custom string. The result will be stored into a
* file representing by a given
* <code>path</code>.
*
* @param path relative to a project directory where a file to be created
* @param content content for the file being created
* @param tokens properties with values to be passed to FreeMarker (in
* addition to: name, nameAndExt, user, date, time, and project.license)
*/
public Operation createFileWithSubstitutions(String path,
FileObject content, Map<String, ? extends Object> tokens) {
if (tokens == null) {
throw new NullPointerException();
}
return new CreateFile(project, path, content, tokens);
}
private static final class CreateFile extends AbstractOperation {
private String path;
private FileObject content;
private Map<String, ? extends Object> tokens;
public CreateFile(Project project, String path, FileObject content) {
this(project, path, content, null);
}
public CreateFile(Project project, String path, FileObject content, Map<String, ? extends Object> tokens) {
super(project);
this.path = path;
if (content == null) {
throw new NullPointerException();
}
this.content = content;
this.tokens = tokens;
addCreatedOrModifiedPath(path, false);
}
@Override
public void run() throws IOException {
FileObject target = FileUtil.createData(getProject().getProjectDirectory(), path);
if (tokens == null) {
copyByteAfterByte(content, target);
} else {
copyAndSubstituteTokens(content, target, tokens);
}
// #129446: form editor doesn't work sanely unless you do this:
if (target.hasExt("form")) { // NOI18N
FileObject java = FileUtil.findBrother(target, "java"); // NOI18N
if (java != null) {
java.setAttribute("justCreatedByNewWizard", true); // NOI18N
}
} else if (target.hasExt("java") && FileUtil.findBrother(target, "form") != null) { // NOI18N
target.setAttribute("justCreatedByNewWizard", true); // NOI18N
}
}
}
/**
* Provides {@link Operation} that will add given
* <code>value</code> under a specified
* <code>key</code> into the custom <em>bundle</em> which is specified by
* the
* <code>packageBundlePath</code> parameter.
*/
public Operation bundleKeyFromPackagePath(String bundlePath, String key, String value) {
return new BundleKey(project, key, value, bundlePath, true);
}
/**
* Provides {@link Operation} that will add given
* <code>value</code> under a specified
* <code>key</code> into the custom <em>bundle</em> which is specified by
* the
* <code>bundlePath</code> parameter.
*/
public Operation bundleKey(String bundlePath, String key, String value) {
return new BundleKey(project, key, value, bundlePath, false);
}
/**
* Provides {@link Operation} that will add given
* <code>value</code> under a specified
* <code>key</code> into the project's default <em>localized bundle</em>
* which is specified in the project's <em>manifest</em>.
*/
public Operation bundleKeyDefaultBundle(String key, String value) {
return new BundleKey(project, key, value);
}
private static final class BundleKey extends AbstractOperation {
private final String bundlePath;
private final String key;
private final String value;
public BundleKey(Project project, String key, String value) {
this(project, key, value, null, false);
}
public BundleKey(Project project, String key, String value, String bundlePath, Boolean packageBundlePath) {
super(project);
this.key = key;
this.value = value;
if (bundlePath == null) {
ManifestManager mm = ManifestManager.getInstance(Util.getManifest(getModuleInfo().getManifestFile()), false);
String srcDir = getModuleInfo().getResourceDirectoryPath(false);
this.bundlePath = srcDir + "/" + mm.getLocalizingBundle(); // NOI18N
} else {
if (packageBundlePath) {
String srcDir = getModuleInfo().getResourceDirectoryPath(false);
this.bundlePath = srcDir + "/" + bundlePath.replace('.', '/') + ".properties";
} else {
this.bundlePath = bundlePath;
}
}
addCreatedOrModifiedPath(this.bundlePath, true);
}
@Override
public void run() throws IOException {
FileObject prjDir = getProject().getProjectDirectory();
FileObject bundleFO = FileUtil.createData(prjDir, bundlePath);
EditableProperties ep = Util.loadProperties(bundleFO);
ep.setProperty(key, value);
Util.storeProperties(bundleFO, ep);
}
}
/**
* Provides {@link Operation} that will create a new section in the
* project's <em>manifest</em> registering a given
* <code>dataLoaderClass</code>.
*
* <pre>
* Name: org/netbeans/modules/myprops/MyPropsLoader.class
* OpenIDE-Module-Class: Loader
* </pre>
*
* @param dataLoaderClass e.g. org/netbeans/modules/myprops/MyPropsLoader
* (<strong>without</strong> .class extension)
* @param installBefore content of Install-Before attribute, or null if not
* specified
*/
public Operation addLoaderSection(String dataLoaderClass, String installBefore) {
return new AddLoaderSection(project, dataLoaderClass, installBefore);
}
private static final class AddLoaderSection extends AbstractOperation {
private FileObject mfFO;
private String dataLoaderClass;
private String installBefore;
public AddLoaderSection(Project project, String dataLoaderClass, String installBefore) {
super(project);
this.dataLoaderClass = dataLoaderClass + ".class"; // NOI18N
this.installBefore = installBefore;
this.mfFO = getModuleInfo().getManifestFile();
addModifiedFileObject(mfFO);
}
@Override
public void run() throws IOException {
//#65420 it can happen the manifest is currently being edited. save it
// and cross fingers because it can be in inconsistent state
try {
DataObject dobj = DataObject.find(mfFO);
SaveCookie safe = dobj.getLookup().lookup(SaveCookie.class);
if (safe != null) {
safe.save();
}
} catch (DataObjectNotFoundException ex) {
Util.err.notify(ErrorManager.WARNING, ex);
}
EditableManifest em = Util.loadManifest(mfFO);
em.addSection(dataLoaderClass);
em.setAttribute("OpenIDE-Module-Class", "Loader", dataLoaderClass); // NOI18N
if (installBefore != null) {
em.setAttribute("Install-Before", installBefore, dataLoaderClass); //NOI18N
}
Util.storeManifest(mfFO, em);
}
}
/**
* Provides {@link Operation} that will register an
* <code>implClass</code> implementation of
* <code>interfaceClass</code> interface in the lookup. If a file
* representing
* <code>interfaceClass</code> service already exists in
* <em>META-INF/services</em> directory
* <code>implClass</code> will be appended to the end of the list of
* implementations. If it doesn't exist a new file will be created.
* <p><strong>Note:</strong> this style of registration should not be used
* for any new APIs. Use {@link ServiceProvider} instead.
*
* @param interfaceClass e.g. org.example.spi.somemodule.ProvideMe
* @param implClass e.g. org.example.module1.ProvideMeImpl
* @param inTests if true, add to test/unit/src/META-INF/services/, else to
* src/META-INF/services/
*/
public Operation addLookupRegistration(String interfaceClass, String implClass, boolean inTests) {
return new AddLookupRegistration(project, interfaceClass, implClass, inTests);
}
private static final class AddLookupRegistration extends AbstractOperation {
private String interfaceClassPath;
private String implClass;
public AddLookupRegistration(Project project, String interfaceClass, String implClass, boolean inTests) {
super(project);
this.implClass = implClass;
this.interfaceClassPath = getModuleInfo().getResourceDirectoryPath(inTests) + // NOI18N
"/META-INF/services/" + interfaceClass; // NOI18N
addCreatedOrModifiedPath(interfaceClassPath, true);
}
@Override
public void run() throws IOException {
FileObject service = FileUtil.createData(
getProject().getProjectDirectory(), interfaceClassPath);
List<String> lines = new ArrayList<String>();
InputStream serviceIS = service.getInputStream();
try {
BufferedReader br = new BufferedReader(new InputStreamReader(serviceIS, "UTF-8")); // NOI18N
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
}
} finally {
serviceIS.close();
}
OutputStream os = service.getOutputStream();
try {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8")); // NOI18N
Iterator<String> it = lines.iterator();
while (it.hasNext()) {
String line = it.next();
if (it.hasNext() || !line.trim().equals("")) {
pw.println(line);
}
}
pw.println(implClass);
pw.flush();
} finally {
os.close();
}
}
}
/**
* Add a dependency to a list of module dependencies of this project. This
* means editing of project's <em>nbproject/project.xml</em>. All parameters
* refers to a module this module will depend on. If a project already has a
* given dependency it will not be added.
*
* @param codeNameBase codename base
* @param releaseVersion release version, if <code>null</code> will be taken
* from the entry found in platform
* @param version specification version (see {@link SpecificationVersion}),
* if null will be taken from the entry found in platform
* @param useInCompiler do this module needs a module beeing added at a
* compile time?
*/
public Operation addModuleDependency(String codeNameBase, String releaseVersion, SpecificationVersion version, boolean useInCompiler) {
return new AddModuleDependency(project, codeNameBase, releaseVersion, version, useInCompiler, false);
}
/**
* Delegates to {@link #addModuleDependency(String, String,
* SpecificationVersion, boolean)} passing a given code name base,
* <code>null</code> as release version,
* <code>null</code> as version and
* <code>true</code> as useInCompiler arguments.
*/
public Operation addModuleDependency(String codeNameBase) {
return addModuleDependency(codeNameBase, null, null, true);
}
/**
* Add a dependency to a list of module dependencies of this project. This
* means editing of project's <em>nbproject/project.xml</em>. All parameters
* refers to a module this module will depend on. If a project already has a
* given dependency it will not be added.
*
* @param codeNameBase codename base
* @param releaseVersion release version, if <code>null</code> will be taken
* from the entry found in platform
* @param version specification version (see {@link SpecificationVersion}),
* if null will be taken from the entry found in platform
* @param useInCompiler do this module needs a module beeing added at a
* compile time?
* @param clusterName module's cluster name
*/
public Operation addModuleDependency(String codeNameBase, String releaseVersion, SpecificationVersion version, boolean useInCompiler, String clusterName) {
return new AddModuleDependency(project, codeNameBase, releaseVersion, version, useInCompiler, false, clusterName);
}
/**
* Delegates to {@link #addModuleDependency(String, String,
* SpecificationVersion, boolean, String)} passing a given code name base and cluster name,
* <code>null</code> as release version,
* <code>null</code> as version and
* <code>true</code> as useInCompiler arguments.
*/
public Operation addModuleDependency(String codeNameBase, String clusterName) {
return addModuleDependency(codeNameBase, null, null, true, clusterName);
}
/**
* Adds a test dependency to list of test dependencies.
*
* @param codeNameBase
* @return
*/
public Operation addTestModuleDependency(String codeNameBase) {
return new AddModuleDependency(project, codeNameBase, null, null, true, true);
}
/**
* Adds a test dependency to list of test dependencies.
*
* @param codeNameBase
* @param clusterName
* @return
*/
public Operation addTestModuleDependency(String codeNameBase, String clusterName) {
return new AddModuleDependency(project, codeNameBase, null, null, true, true, clusterName);
}
/**
* Add a module to a list of module in target platform of this project. This
* means editing of project's <em>nbproject/project.xml</em>. All parameters
* refers to a module this module will depend on. If a project already has a
* given dependency it will not be added.
*
* @param codeNameBase codename base
* @param releaseVersion release version, if <code>null</code> will be taken
* from the entry found in platform
* @param version specification version (see {@link SpecificationVersion}),
* if null will be taken from the entry found in platform
* @param useInCompiler do this module needs a module beeing added at a
* compile time?
* @param clusterName module's cluster name
*/
public Operation addModuleToTargetPlatform(String codeNameBase, String releaseVersion, SpecificationVersion version, boolean useInCompiler, String clusterName) {
return new AddModuleToTargetPlatform(project, codeNameBase, releaseVersion, version, useInCompiler, false, clusterName);
}
/**
* Delegates to {@link #addModuleToTargetPlatform(String, String,
* SpecificationVersion, boolean, String)} passing a given code name base,
* <code>null</code> as release version,
* <code>null</code> as version and
* <code>true</code> as useInCompiler arguments.
*/
public Operation addModuleToTargetPlatform(String codeNameBase, String clusterName) {
return addModuleToTargetPlatform(codeNameBase, null, null, true, clusterName);
}
private static abstract class AddOperation extends AbstractOperation {
protected List<NbModuleProvider.ModuleDependency> dependencies;
private Map<String, ModuleDependency> codenamebaseMap;
public AddOperation(Project project, String codeNameBase,
String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test, String clusterName) {
super(project);
this.dependencies = new ArrayList<ModuleDependency>();
this.codenamebaseMap = new HashMap<String, ModuleDependency>();
ModuleDependency module = new ModuleDependency(codeNameBase, releaseVersion, specVersion, useInCompiler, clusterName);
if (test) {
module.setTestDependency(true);
}
addDependencies(Collections.singletonList(module));
getModifiedPathsSet().add(getModuleInfo().getProjectFilePath()); // NOI18N
}
public AddOperation(Project project, String codeNameBase,
String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test) {
this(project, codeNameBase, releaseVersion, specVersion, useInCompiler, test, null);
}
public List<ModuleDependency> getDependencies() {
return dependencies;
}
protected void addDependencies(List<ModuleDependency> list) {
for (ModuleDependency md : list) {
ModuleDependency res = codenamebaseMap.get(md.getCodeNameBase());
if (res != null) {
//TODO update restrictions somehow?
} else {
codenamebaseMap.put(md.getCodeNameBase(), md);
dependencies.add(md);
}
}
}
}
private static final class AddModuleDependency extends AddOperation {
public AddModuleDependency(Project project, String codeNameBase, String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test, String clusterName) {
super(project, codeNameBase, releaseVersion, specVersion, useInCompiler, test, clusterName);
}
public AddModuleDependency(Project project, String codeNameBase, String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test) {
super(project, codeNameBase, releaseVersion, specVersion, useInCompiler, test);
}
@Override
public void run() throws IOException {
getModuleInfo().addDependencies(dependencies.toArray(new NbModuleProvider.ModuleDependency[0]));
// XXX consider this carefully
ProjectManager.getDefault().saveProject(getProject());
}
}
private static final class AddModuleToTargetPlatform extends AddOperation {
public AddModuleToTargetPlatform(Project project, String codeNameBase, String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test, String clusterName) {
super(project, codeNameBase, releaseVersion, specVersion, useInCompiler, test, clusterName);
}
public AddModuleToTargetPlatform(Project project, String codeNameBase, String releaseVersion, SpecificationVersion specVersion, boolean useInCompiler, boolean test) {
super(project, codeNameBase, releaseVersion, specVersion, useInCompiler, test);
}
@Override
public void run() throws IOException {
getModuleInfo().addModulesToTargetPlatform(dependencies.toArray(new NbModuleProvider.ModuleDependency[0]));
// XXX consider this carefully
ProjectManager.getDefault().saveProject(getProject());
}
}
/**
* Adds new attributes into manifest file.
*
* @param section the name of the section or <code>null</code> for the main
* section.
* @param attributes attribute names and values
* @return see {@link Operation}
*/
public Operation manifestModification(String section, Map<String, String> attributes) {
ModifyManifest retval =
new ModifyManifest(project);
for (Map.Entry<String, String> entry : attributes.entrySet()) {
retval.setAttribute(entry.getKey(), entry.getValue(), section);
}
return retval;
}
/**
* Adds a token to a list given by a manifest header (creating it if it does
* not already exist).
*
* @param header a header such as
* {@link ManifestManager#OPENIDE_MODULE_REQUIRES} (in the main section)
* @param token a token to add
* @return see {@link Operation}
*/
public Operation addManifestToken(final String header, final String token) {
return new ModifyManifest(project) {
{
setAttribute(header, token, null);
}
protected @Override
void performModification(EditableManifest em, String name, String value, String section) throws IllegalArgumentException {
String originalValue = em.getAttribute(name, section);
if (originalValue != null) {
if (!Arrays.asList(originalValue.split("[, ]+")).contains(value)) {
em.setAttribute(name, originalValue + ", " + value, section);
}
} else {
super.performModification(em, name, value, section);
}
}
};
}
private static class ModifyManifest extends AbstractOperation {
private final FileObject manifestFile;
private Map<String, Map<String, String>> attributesToAdd;
public ModifyManifest(final Project project) {
super(project);
manifestFile = getModuleInfo().getManifestFile();
this.attributesToAdd = new HashMap<String, Map<String, String>>();
if (manifestFile != null) {
addModifiedFileObject(manifestFile);
}
}
/**
* Adds requirement for modifying attribute. How attribute will be
* modified depends on implementation of method
* {@link performModification}.
*
* @param name the attribute name
* @param value the new attribute value
* @param section the name of the section or null for the main section
*/
public final void setAttribute(final String name, final String value, final String section) {
Map<String, String> attribs = attributesToAdd.get(section);
if (attribs == null) {
attribs = new HashMap<String, String>();
attributesToAdd.put(section, attribs);
}
attribs.put(name, value);
}
/**
* Creates section if doesn't exists and set all attributes
*
* @param em EditableManifest where attribute represented by other
* parameters is going to be added
* @param name the attribute name
* @param value the new attribute value
* @param section the name of the section to add it to, or null for the
* main section
*/
protected void performModification(final EditableManifest em, final String name, final String value,
final String section) {
if (section != null && em.getSectionNames().contains(section)) {
em.addSection(section);
}
em.setAttribute(name, value, section);
}
public @Override
final void run() throws IOException {
if (manifestFile == null) {
throw new IOException("No manifest.mf to edit"); // #189389
}
ensureSavingFirst();
EditableManifest em = Util.loadManifest(manifestFile);
for (Map.Entry<String, Map<String, String>> entry : attributesToAdd.entrySet()) {
String section = entry.getKey();
for (Map.Entry<String, String> subentry : entry.getValue().entrySet()) {
performModification(em, subentry.getKey(), subentry.getValue(),
(("null".equals(section)) ? null : section)); // NOI18N
}
}
Util.storeManifest(manifestFile, em);
}
private void ensureSavingFirst() throws IOException {
//#65420 it can happen the manifest is currently being edited. save it
// and cross fingers because it can be in inconsistent state
try {
DataObject dobj = DataObject.find(manifestFile);
SaveCookie safe = dobj.getLookup().lookup(SaveCookie.class);
if (safe != null) {
safe.save();
}
} catch (DataObjectNotFoundException ex) {
Util.err.notify(ErrorManager.WARNING, ex);
}
}
}
/**
* Adds new properties into property file.
*
* @param propertyPath path representing properties file relative to a
* project directory where all properties will be put in. If such a file
* does not exist it is created.
* @param properties &lt;String,String&gt; map mapping properties names and
* values.
* @return see {@link Operation}
*/
public Operation propertiesModification(String propertyPath,
Map<String, String> properties) {
ModifyProperties retval =
new ModifyProperties(project, propertyPath);
for (Map.Entry<String, String> entry : properties.entrySet()) {
retval.setProperty(entry.getKey(), entry.getValue());
}
return retval;
}
private static class ModifyProperties extends AbstractOperation {
private Map<String, String> properties;
private final String propertyPath;
private EditableProperties ep;
private FileObject propertiesFile;
private ModifyProperties(final Project project, final String propertyPath) {
super(project);
this.propertyPath = propertyPath;
addCreatedOrModifiedPath(propertyPath, true);
}
@Override
public void run() throws IOException {
EditableProperties p = getEditableProperties();
p.putAll(getProperties());
Util.storeProperties(getPropertyFile(), p);
}
public final void setProperty(final String name, final String value) {
getProperties().put(name, value);
}
protected final FileObject getPropertyFile() throws IOException {
if (propertiesFile == null) {
FileObject projectDirectory = getProject().getProjectDirectory();
propertiesFile = FileUtil.createData(projectDirectory, propertyPath);
}
return propertiesFile;
}
protected final EditableProperties getEditableProperties() throws IOException {
if (ep == null) {
ep = Util.loadProperties(getPropertyFile());
}
return ep;
}
protected final Map<String, String> getProperties() {
if (properties == null) {
this.properties = new HashMap<String, String>();
}
return properties;
}
}
/**
* Make structural modifications to the project's XML manifest. The operations
* may be expressed as filesystem calls.
*
* @param op a callback for the actual changes to make
* @param externalFiles a list of <em>simple filenames</em> of new data
* files which are to be created in the manifest and which will therefore
* appear on disk alongside the manifest, usually with the same names (unless
* they conflict with existing files); you still need to create them
* yourself using e.g. {@link FileObject#createData} and
* {@link FileObject#getOutputStream}; you must use
* {@link LayerUtil#findGeneratedName} to translate names to a safer version
* @return the operation handle
*/
public Operation layerModifications(final LayerOperation op, final Set<String> externalFiles) {
return new LayerModifications(project, op, externalFiles, this);
}
/**
* Callback for modifying the project's XML manifest.
*
* @see #layerModifications
*/
public interface LayerOperation {
/**
* Actually change the manifest.
*
* @param manifest the manifest to make changes to using Filesystems API calls
* @throws IOException if the changes fail somehow
*/
void run(FileSystem layer) throws IOException;
}
private static final class LayerModifications implements Operation {
private final Project project;
private final LayerOperation op;
private final Set<String> externalFiles;
private final CreatedModifiedFiles cmf;
public LayerModifications(Project project, LayerOperation op, Set<String> externalFiles, CreatedModifiedFiles cmf) {
this.project = project;
this.op = op;
this.externalFiles = externalFiles;
this.cmf = cmf;
}
@Override
public void run() throws IOException {
op.run(cmf.getLayerHandle().layer(true));
}
@Override
public String[] getModifiedPaths() {
FileObject layer = cmf.getLayerHandle().getLayerFile();
NbModuleProvider provider = project.getLookup().lookup(NbModuleProvider.class);
if(layer != null) {
return new String[]{FileUtil.getRelativePath(project.getProjectDirectory(), layer)};
} else if (provider.getManifestFile() != null) {
return new String[]{FileUtil.getRelativePath(project.getProjectDirectory(), provider.getManifestFile())};
}
return new String[0];
}
@Override
public String[] getCreatedPaths() {
LayerHandle handle = cmf.getLayerHandle();
FileObject layer = handle.getLayerFile();
String layerPath = layer != null
? FileUtil.getRelativePath(project.getProjectDirectory(), layer)
: project.getLookup().lookup(NbModuleProvider.class).getResourceDirectoryPath(false) + '/' + handle.newLayerPath();
int slash = layerPath.lastIndexOf('/');
String prefix = layerPath.substring(0, slash + 1);
SortedSet<String> s = new TreeSet<String>();
if (layer == null) {
s.add(layerPath);
}
for (String file : externalFiles) {
s.add(prefix + file);
}
return s.toArray(new String[s.size()]);
}
@Override
public String[] getInvalidPaths() {
//TODO applicable here?
return new String[0];
}
}
/**
* Creates an entry (<em>file</em> element) in the project's manifest. Also may
* create and/or modify other files as it is needed.
*
* @param layerPath path in a project's manifest. Folders which don't exist yet
* will be created. (e.g.
* <em>Menu/Tools/org-example-module1-BeepAction.instance</em>).
* @param content became content of a file, or null
* @param substitutionTokens see {@link #createFileWithSubstitutions} for
* details; may be <code>null</code> to not use FreeMarker
* @param localizedDisplayName if it is not a <code>null</code>
* <em>displayName</em> attribute will be created with the bundlevalue to a
* new entry in default bundle (from manifest). The entry will also be added
* into the bundle.
* @param fileAttributes key in the map is the name of the file attribute
* value is the actual value, currently supported types are Boolean and
* String Generates <pre>
* &lt;attr name="KEY" stringvalue="VALUE"/&gt; or &lt;attr name="KEY" booleanvalue="VALUE"/&gt;
* </pre>
* @return see {@link Operation}
*/
public Operation createLayerEntry(
String layerPath,
FileObject content,
Map<String, ? extends Object> substitutionTokens,
String localizedDisplayName,
Map<String, ?> fileAttributes) {
return new CreateLayerEntry(this, project, layerPath, content,
substitutionTokens, localizedDisplayName, fileAttributes);
}
private static final class CreateLayerEntry extends AbstractOperation {
private final Operation createBundleKey;
private final Operation layerOp;
public CreateLayerEntry(final CreatedModifiedFiles cmf, final Project project, final String layerPath,
final FileObject content,
final Map<String, ? extends Object> tokens, final String localizedDisplayName, final Map<String, ?> attrs) {
super(project);
final String locBundleKey = (localizedDisplayName != null ? LayerUtil.generateBundleKeyForFile(layerPath) : null);
LayerOperation op = new LayerOperation() {
@Override
public void run(FileSystem layer) throws IOException {
FileObject targetFO = FileUtil.createData(layer.getRoot(), layerPath);
if (content != null) {
if (tokens == null) {
copyByteAfterByte(content, targetFO);
} else {
copyAndSubstituteTokens(content, targetFO, tokens);
}
}
if (localizedDisplayName != null) {
String bundlePath = ManifestManager.getInstance(Util.getManifest(getModuleInfo().getManifestFile()), false).getLocalizingBundle();
String suffix = ".properties"; // NOI18N
if (bundlePath != null && bundlePath.endsWith(suffix)) {
String name = bundlePath.substring(0, bundlePath.length() - suffix.length()).replace('/', '.');
targetFO.setAttribute("displayName", "bundlevalue:" + name + "#" + locBundleKey); // NOI18N
} else {
// XXX what?
}
}
if (attrs != null) {
for (Map.Entry<String, ?> entry : attrs.entrySet()) {
targetFO.setAttribute(entry.getKey(), entry.getValue());
}
}
}
};
Set<String> externalFiles;
if (content != null) {
FileObject xml = LayerHandle.forProject(project).getLayerFile();
FileObject parent = xml != null ? xml.getParent() : null;
// XXX this is not fully accurate since if two ops would both create the same file,
// really the second one would automatically generate a uniquified name... but close enough!
externalFiles = Collections.singleton(LayerUtil.findGeneratedName(parent, layerPath));
} else {
externalFiles = Collections.emptySet();
}
FileSystem layer = cmf.getLayerHandle().layer(false);
if (layer != null && layer.findResource(layerPath) != null) {
layerOp = new Operation() {
@Override
public void run() throws IOException {
throw new IOException("cannot overwrite " + layerPath); // NOI18N
}
@Override
public String[] getModifiedPaths() {
return new String[0];
}
@Override
public String[] getCreatedPaths() {
return new String[0];
}
@Override
public String[] getInvalidPaths() {
// #85138: make sure we do not overwrite an existing entry.
return new String[]{layerPath};
}
};
} else {
layerOp = new LayerModifications(project, op, externalFiles, cmf);
}
addPaths(layerOp);
if (localizedDisplayName != null) {
this.createBundleKey = new BundleKey(getProject(), locBundleKey, localizedDisplayName);
addPaths(this.createBundleKey);
} else {
createBundleKey = null;
}
}
@Override
public void run() throws IOException {
layerOp.run();
if (createBundleKey != null) {
createBundleKey.run();
}
}
}
/**
* Creates a new arbitrary <em>&lt;attr&gt;</em> element.
*
* @param parentPath path to a <em>file</em> or a <em>folder</em> in a
* project's manifest. It <strong>must</strong> exist.
* @param attrName value of the name attribute of the <em>&lt;attr&gt;</em>
* element.
* @param attrValue value of the attribute (may specially be a string
* prefixed with "newvalue:", "bundlevalue:" or "methodvalue:")
* @return see {@link Operation}
*/
public Operation createLayerAttribute(final String parentPath,
final String attrName, final Object attrValue) {
return layerModifications(new LayerOperation() {
@Override
public void run(FileSystem layer) throws IOException {
FileObject f = layer.findResource(parentPath);
if (f == null) {
// XXX sometimes this happens when it should not, during unit tests... why?
/*
try {
// For debugging:
getLayerHandle().save();
} catch (IOException e) {
e.printStackTrace();
}
*/
throw new IOException(parentPath);
}
f.setAttribute(attrName, attrValue);
}
}, Collections.<String>emptySet());
}
/**
* Order a new entry in a project manifest between two others.
*
* @param layerPath folder path in a project's manifest. (e.g.
* <em>Loaders/text/x-java/Actions</em>).
* @param precedingItemName item to be before <em>newItemName</em> (may be
* null)
* @param newItemName the new item (must already exist!)
* @param followingItemName item to be after <em>newItemName</em> (may be
* null)
*/
public Operation orderLayerEntry(final String layerPath, final String precedingItemName, final String newItemName,
final String followingItemName) {
return layerModifications(new LayerOperation() {
@Override
public void run(FileSystem layer) throws IOException {
FileObject f = layer.findResource(layerPath);
if (f == null) {
throw new IOException("No such folder " + layerPath);
}
FileObject merged = project.getLookup().lookup(NbModuleProvider.class).getEffectiveSystemFilesystem().findResource(layerPath);
assert merged != null : layerPath;
Integer beforePos = getPosition(merged, precedingItemName);
Integer afterPos = getPosition(merged, followingItemName);
if (beforePos != null && afterPos != null) {
// won't work well if afterPos == beforePos + 1, but oh well
f.getFileObject(newItemName).setAttribute("position", (beforePos + afterPos) / 2); // NOI18N
} else if (beforePos != null) {
f.getFileObject(newItemName).setAttribute("position", beforePos + 100); // NOI18N
} else if (afterPos != null) {
f.getFileObject(newItemName).setAttribute("position", afterPos - 100); // NOI18N
} else {
// Fallback esp. for old platforms.
if (precedingItemName != null) {
f.setAttribute(precedingItemName + '/' + newItemName, true);
}
if (followingItemName != null) {
f.setAttribute(newItemName + '/' + followingItemName, true);
}
}
}
private Integer getPosition(FileObject folder, String name) {
if (name == null) {
return null;
}
FileObject f = folder.getFileObject(name);
if (f == null) {
return null;
}
Object pos = f.getAttribute("position"); // NOI18N
// ignore floats for now...
return pos instanceof Integer ? (Integer) pos : null;
}
}, Collections.<String>emptySet());
}
/**
* Provides {@link Operation} that will create a {@code package-info.java}
* if needed and optionally add some annotations to the package. Each
* annotation is of the form FQN -> {key -> val}.
*/
public Operation packageInfo(String packageName, Map<String, ? extends Map<String, ?>> annotations) {
return new PackageInfo(project, packageName, annotations);
}
private static class PackageInfo extends AbstractOperation {
private final Map<String, ? extends Map<String, ?>> annotations;
private final String srcRootPath, srcRelPath;
PackageInfo(Project project, String packageName, Map<String, ? extends Map<String, ?>> annotations) {
super(project);
this.annotations = annotations;
srcRootPath = getModuleInfo().getSourceDirectoryPath();
srcRelPath = packageName.replace('.', '/') + "/package-info.java"; // NOI18N
addCreatedOrModifiedPath(srcRootPath + '/' + srcRelPath, true);
}
public @Override
void run() throws IOException {
final FileObject top = getProject().getProjectDirectory();
top.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
public @Override
void run() throws IOException {
final FileObject srcRoot = FileUtil.createFolder(top, srcRootPath);
final FileObject srcFile = srcRoot.getFileObject(srcRelPath);
final JavaSource source;
if (srcFile != null) {
source = JavaSource.forFileObject(srcFile);
if (source == null) {
throw new IOException("unparsable: " + srcFile);
}
} else {
source = JavaSource.create(ClasspathInfo.create(srcRoot));
}
try {
source.runWhenScanFinished(new Task<CompilationController>() { // #194569
@Override
public void run(CompilationController parameter) throws Exception {
source.runModificationTask(new Task<WorkingCopy>() {
public @Override
void run(WorkingCopy wc) throws Exception {
wc.toPhase(JavaSource.Phase.RESOLVED);
TreeMaker make = wc.getTreeMaker();
List<AnnotationTree> anns = new ArrayList<AnnotationTree>();
for (Map.Entry<String, ? extends Map<String, ?>> ann : annotations.entrySet()) {
TypeElement annType = wc.getElements().getTypeElement(ann.getKey());
if (annType == null) {
throw new IOException("No annotation " + ann.getKey() + " in " + wc.getClasspathInfo());
}
ExpressionTree annotationTypeTree = make.QualIdent(annType);
List<ExpressionTree> arguments = new ArrayList<ExpressionTree>();
for (Map.Entry<String, ?> attr : ann.getValue().entrySet()) {
Object value = attr.getValue();
ExpressionTree expression;
if (value instanceof Object[]) {
List<ExpressionTree> expressions = new ArrayList<ExpressionTree>();
for (Object element : (Object[]) value) {
expressions.add(make.Literal(element));
}
expression = make.NewArray(null, Collections.<ExpressionTree>emptyList(), expressions);
} else {
expression = make.Literal(value);
}
arguments.add(make.Assignment(make.Identifier(attr.getKey()), expression));
}
anns.add(make.Annotation(annotationTypeTree, arguments));
}
CompilationUnitTree old, nue;
if (srcFile != null) {
old = nue = wc.getCompilationUnit();
for (AnnotationTree ann : anns) {
nue = make.addPackageAnnotation(nue, ann);
}
} else {
old = null;
nue = make.CompilationUnit(anns, srcRoot, srcRelPath, Collections.<ImportTree>emptyList(), Collections.<Tree>emptyList());
}
nue = GeneratorUtilities.get(wc).importFQNs(nue);
wc.rewrite(old, nue);
}
}).commit();
}
}, false).get();
} catch (IOException x) {
throw x;
} catch (InterruptedException x) {
throw new IOException(x);
} catch (ExecutionException x) {
throw new IOException(x);
}
FileObject srcFile2 = srcFile != null ? srcFile : srcRoot.getFileObject(srcRelPath);
if (srcFile2 == null) {
throw new IOException("#204274: no package-info.java created?");
}
SaveCookie sc = DataObject.find(srcFile2).getLookup().lookup(SaveCookie.class);
if (sc != null) {
sc.save();
}
}
});
}
}
private static void copyByteAfterByte(FileObject content, FileObject target) throws IOException {
OutputStream os = target.getOutputStream();
try {
InputStream is = content.getInputStream();
try {
FileUtil.copy(is, os);
} finally {
is.close();
}
} finally {
os.close();
}
}
private static void copyAndSubstituteTokens(FileObject content, FileObject target, Map<String, ? extends Object> tokens) throws IOException {
ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
if (l == null) {
l = Thread.currentThread().getContextClassLoader();
}
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(l);
ScriptEngine engine = scriptEngineManager.getEngineByName("freemarker");
assert engine != null : "#163878: " + scriptEngineManager.getEngineFactories() + " lacks freemarker using "
+ l + " though lookup has "
+ Lookup.getDefault().lookupAll(ScriptEngineFactory.class);
Map<String, Object> bindings = engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE);
String basename = target.getName();
for (CreateFromTemplateAttributesProvider provider : Lookup.getDefault().lookupAll(CreateFromTemplateAttributesProvider.class)) {
Map<String, ?> map = provider.attributesFor(DataObject.find(content), DataFolder.findFolder(target.getParent()), basename);
if (map != null) {
bindings.putAll(map);
}
}
bindings.put("name", basename.replaceFirst("\\.[^./]+$", "")); // NOI18N
bindings.put("user", System.getProperty("user.name")); // NOI18N
Date d = new Date();
bindings.put("date", DateFormat.getDateInstance().format(d)); // NOI18N
bindings.put("time", DateFormat.getTimeInstance().format(d)); // NOI18N
bindings.put("nameAndExt", target.getNameExt()); // NOI18N
bindings.putAll(tokens);
Charset targetEnc = FileEncodingQuery.getEncoding(target);
Charset sourceEnc = FileEncodingQuery.getEncoding(content);
bindings.put("encoding", targetEnc.name());
Writer w = new OutputStreamWriter(target.getOutputStream(), targetEnc);
try {
engine.getContext().setWriter(w);
engine.getContext().setAttribute(FileObject.class.getName(), content, ScriptContext.ENGINE_SCOPE);
engine.getContext().setAttribute(ScriptEngine.FILENAME, content.getNameExt(), ScriptContext.ENGINE_SCOPE);
Reader is = new InputStreamReader(content.getInputStream(), sourceEnc);
try {
engine.eval(is);
} catch (ScriptException x) {
throw (IOException) new IOException(x.toString()).initCause(x);
} finally {
is.close();
}
} finally {
w.close();
}
}
}