21131e5882e9f0dbad0c422d758c4c8696e49bc8
[idea/community.git] / java / idea-ui / src / com / intellij / platform / templates / SaveProjectAsTemplateAction.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.platform.templates;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.codeInspection.defaultFileTemplateUsage.FileHeaderChecker;
20 import com.intellij.ide.fileTemplates.FileTemplate;
21 import com.intellij.ide.fileTemplates.FileTemplateManager;
22 import com.intellij.ide.impl.ProjectUtil;
23 import com.intellij.ide.util.projectWizard.ProjectTemplateFileProcessor;
24 import com.intellij.ide.util.projectWizard.ProjectTemplateParameterFactory;
25 import com.intellij.openapi.actionSystem.AnAction;
26 import com.intellij.openapi.actionSystem.AnActionEvent;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.application.WriteAction;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.extensions.Extensions;
31 import com.intellij.openapi.module.Module;
32 import com.intellij.openapi.progress.PerformInBackgroundOption;
33 import com.intellij.openapi.progress.ProgressIndicator;
34 import com.intellij.openapi.progress.ProgressManager;
35 import com.intellij.openapi.progress.Task;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.roots.ContentIterator;
38 import com.intellij.openapi.roots.FileIndex;
39 import com.intellij.openapi.roots.ModuleRootManager;
40 import com.intellij.openapi.roots.ProjectRootManager;
41 import com.intellij.openapi.ui.Messages;
42 import com.intellij.openapi.util.JDOMUtil;
43 import com.intellij.openapi.util.io.FileUtil;
44 import com.intellij.openapi.util.io.StreamUtil;
45 import com.intellij.openapi.vfs.CharsetToolkit;
46 import com.intellij.openapi.vfs.VfsUtil;
47 import com.intellij.openapi.vfs.VfsUtilCore;
48 import com.intellij.openapi.vfs.VirtualFile;
49 import com.intellij.util.io.ZipUtil;
50 import com.intellij.util.ui.UIUtil;
51 import gnu.trove.TIntObjectHashMap;
52 import org.jdom.Element;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import java.io.*;
57 import java.util.HashMap;
58 import java.util.Map;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61 import java.util.zip.ZipEntry;
62 import java.util.zip.ZipOutputStream;
63
64 /**
65  * @author Dmitry Avdeev
66  *         Date: 10/5/12
67  */
68 public class SaveProjectAsTemplateAction extends AnAction {
69
70   private static final Logger LOG = Logger.getInstance(SaveProjectAsTemplateAction.class);
71   private static final String PROJECT_TEMPLATE_XML = "project-template.xml";
72
73   @Override
74   public void actionPerformed(AnActionEvent e) {
75     final Project project = getEventProject(e);
76     assert project != null;
77     if (!ProjectUtil.isDirectoryBased(project)) {
78       Messages.showErrorDialog(project, "Project templates do not support old .ipr (file-based) format.\n" +
79                                         "Please convert your project via File->Save as Directory-Based format.", CommonBundle.getErrorTitle());
80       return;
81     }
82
83     final VirtualFile descriptionFile = getDescriptionFile(project, LocalArchivedTemplate.DESCRIPTION_PATH);
84     final SaveProjectAsTemplateDialog dialog = new SaveProjectAsTemplateDialog(project, descriptionFile);
85
86     if (dialog.showAndGet()) {
87
88       final Module moduleToSave = dialog.getModuleToSave();
89       final File file = dialog.getTemplateFile();
90       final String description = dialog.getDescription();
91
92       ProgressManager.getInstance().run(new Task.Backgroundable(project, "Saving Project as Template", true, PerformInBackgroundOption.DEAF) {
93         @Override
94         public void run(@NotNull final ProgressIndicator indicator) {
95           saveProject(project, file, moduleToSave, description, dialog.isReplaceParameters(), indicator);
96         }
97
98         @Override
99         public void onSuccess() {
100           Messages.showInfoMessage(FileUtil.getNameWithoutExtension(file) + " was successfully created.\n" +
101                                    "It's available now in Project Wizard", "Template Created");
102         }
103
104         @Override
105         public void onCancel() {
106           file.delete();
107         }
108       });
109     }
110   }
111
112   public static VirtualFile getDescriptionFile(Project project, String path) {
113     VirtualFile baseDir = project.getBaseDir();
114     return baseDir != null ? baseDir.findFileByRelativePath(path) : null;
115   }
116
117   public static void saveProject(final Project project,
118                                  final File zipFile,
119                                  Module moduleToSave,
120                                  final String description,
121                                  boolean replaceParameters,
122                                  final ProgressIndicator indicator) {
123
124     final Map<String, String> parameters = computeParameters(project, replaceParameters);
125     indicator.setText("Saving project...");
126     ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(project::save),
127                                                       indicator.getModalityState());
128     indicator.setText("Processing project files...");
129     ZipOutputStream stream = null;
130     try {
131       FileUtil.ensureExists(zipFile.getParentFile());
132       stream = new ZipOutputStream(new FileOutputStream(zipFile));
133
134       final VirtualFile dir = getDirectoryToSave(project, moduleToSave);
135       writeFile(LocalArchivedTemplate.DESCRIPTION_PATH, description, project, dir, stream, true, indicator);
136       if (replaceParameters) {
137         String text = getInputFieldsText(parameters);
138         writeFile(LocalArchivedTemplate.TEMPLATE_DESCRIPTOR, text, project, dir, stream, false, indicator);
139       }
140
141       FileIndex index = moduleToSave == null
142                         ? ProjectRootManager.getInstance(project).getFileIndex()
143                         : ModuleRootManager.getInstance(moduleToSave).getFileIndex();
144       final ZipOutputStream finalStream = stream;
145
146       index.iterateContent(new ContentIterator() {
147         @Override
148         public boolean processFile(final VirtualFile virtualFile) {
149           if (!virtualFile.isDirectory()) {
150             final String fileName = virtualFile.getName();
151             indicator.setText2(fileName);
152             try {
153               String relativePath = VfsUtilCore.getRelativePath(virtualFile, dir, '/');
154               if (relativePath == null) {
155                 throw new RuntimeException("Can't find relative path for " + virtualFile + " in " + dir);
156               }
157               final boolean system = Project.DIRECTORY_STORE_FOLDER.equals(virtualFile.getParent().getName());
158               if (system) {
159                 if (!fileName.equals("description.html") &&
160                     !fileName.equals(PROJECT_TEMPLATE_XML) &&
161                     !fileName.equals("misc.xml") &&
162                     !fileName.equals("modules.xml") &&
163                     !fileName.equals("workspace.xml")) {
164                   return true;
165                 }
166               }
167
168               ZipUtil.addFileToZip(finalStream, new File(virtualFile.getPath()), dir.getName() + "/" + relativePath, null, null, new ZipUtil.FileContentProcessor() {
169                 @Override
170                 public InputStream getContent(final File file) throws IOException {
171                   if (virtualFile.getFileType().isBinary() || PROJECT_TEMPLATE_XML.equals(virtualFile.getName())) return STANDARD.getContent(file);
172                   String result = getEncodedContent(virtualFile, project, parameters);
173                   return new ByteArrayInputStream(result.getBytes(CharsetToolkit.UTF8_CHARSET));
174                 }
175               });
176             }
177             catch (IOException e) {
178               LOG.error(e);
179             }
180           }
181           indicator.checkCanceled();
182           return true;
183         }
184       });
185     }
186     catch (Exception ex) {
187       LOG.error(ex);
188       UIUtil.invokeLaterIfNeeded(new Runnable() {
189         public void run() {
190           Messages.showErrorDialog(project, "Can't save project as template", "Internal Error");
191         }
192       });
193     }
194     finally {
195       StreamUtil.closeStream(stream);
196     }
197   }
198
199   private static void writeFile(String path,
200                                 final String text,
201                                 Project project, VirtualFile dir, ZipOutputStream stream, boolean overwrite, ProgressIndicator indicator) throws IOException {
202     final VirtualFile descriptionFile = getDescriptionFile(project, path);
203     if (descriptionFile == null) {
204       stream.putNextEntry(new ZipEntry(dir.getName() + "/" + path));
205       stream.write(text.getBytes());
206       stream.closeEntry();
207     }
208     else if (overwrite) {
209       ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(() -> {
210         try {
211           VfsUtil.saveText(descriptionFile, text);
212         }
213         catch (IOException e) {
214           LOG.error(e);
215         }
216       }), indicator.getModalityState());
217     }
218   }
219
220   public static Map<String, String> computeParameters(final Project project, boolean replaceParameters) {
221     final Map<String, String> parameters = new HashMap<String, String>();
222     if (replaceParameters) {
223       ApplicationManager.getApplication().runReadAction(new Runnable() {
224         public void run() {
225           ProjectTemplateParameterFactory[] extensions = Extensions.getExtensions(ProjectTemplateParameterFactory.EP_NAME);
226           for (ProjectTemplateParameterFactory extension : extensions) {
227             String value = extension.detectParameterValue(project);
228             if (value != null) {
229               parameters.put(value, extension.getParameterId());
230             }
231           }
232         }
233       });
234     }
235     return parameters;
236   }
237
238   public static String getEncodedContent(VirtualFile virtualFile,
239                                           Project project,
240                                           Map<String, String> parameters) throws IOException {
241     String text = VfsUtilCore.loadText(virtualFile);
242     final FileTemplate template = FileTemplateManager.getInstance(project).getDefaultTemplate(FileTemplateManager.FILE_HEADER_TEMPLATE_NAME);
243     final String templateText = template.getText();
244     final Pattern pattern = FileHeaderChecker.getTemplatePattern(template, project, new TIntObjectHashMap<String>());
245     String result = convertTemplates(text, pattern, templateText);
246     result = ProjectTemplateFileProcessor.encodeFile(result, virtualFile, project);
247     for (Map.Entry<String, String> entry : parameters.entrySet()) {
248       result = result.replace(entry.getKey(), "${" + entry.getValue() + "}");
249     }
250     return result;
251   }
252
253   private static VirtualFile getDirectoryToSave(Project project, @Nullable Module module) {
254     if (module == null) {
255       return project.getBaseDir();
256     }
257     else {
258       VirtualFile moduleFile = module.getModuleFile();
259       assert moduleFile != null;
260       return moduleFile.getParent();
261     }
262   }
263
264   public static String convertTemplates(String input, Pattern pattern, String template) {
265     Matcher matcher = pattern.matcher(input);
266     int start = matcher.matches() ? matcher.start(1) : -1;
267     StringBuilder builder = new StringBuilder(input.length() + 10);
268     for (int i = 0; i < input.length(); i++) {
269       if (start == i) {
270         builder.append(template);
271         //noinspection AssignmentToForLoopParameter
272         i = matcher.end(1);
273       }
274
275       char c = input.charAt(i);
276       if (c == '$' || c == '#') {
277         builder.append('\\');
278       }
279       builder.append(c);
280     }
281     return builder.toString();
282   }
283
284   private static String getInputFieldsText(Map<String, String> parameters) {
285     Element element = new Element(RemoteTemplatesFactory.TEMPLATE);
286     for (Map.Entry<String, String> entry : parameters.entrySet()) {
287       Element field = new Element(ArchivedProjectTemplate.INPUT_FIELD);
288       field.setText(entry.getValue());
289       field.setAttribute(RemoteTemplatesFactory.INPUT_DEFAULT, entry.getKey());
290       element.addContent(field);
291     }
292     return JDOMUtil.writeElement(element);
293   }
294
295   @Override
296   public void update(AnActionEvent e) {
297     Project project = getEventProject(e);
298     e.getPresentation().setEnabled(project != null && !project.isDefault());
299   }
300 }