1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 
17 package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
18 
19 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
20 import static com.android.SdkConstants.GRID_LAYOUT;
21 
22 import com.android.SdkConstants;
23 import com.android.ide.common.resources.configuration.FolderConfiguration;
24 import com.android.ide.common.xml.XmlFormatStyle;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.AdtUtils;
27 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
28 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
29 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
30 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
32 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
33 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
34 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
35 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
36 import com.android.resources.ResourceFolderType;
37 import com.android.utils.Pair;
38 
39 import org.eclipse.core.resources.IFile;
40 import org.eclipse.core.resources.IProject;
41 import org.eclipse.core.resources.IResource;
42 import org.eclipse.core.runtime.CoreException;
43 import org.eclipse.core.runtime.IPath;
44 import org.eclipse.core.runtime.IStatus;
45 import org.eclipse.core.runtime.Path;
46 import org.eclipse.jface.resource.ImageDescriptor;
47 import org.eclipse.jface.text.IRegion;
48 import org.eclipse.jface.text.Region;
49 import org.eclipse.jface.viewers.IStructuredSelection;
50 import org.eclipse.jface.wizard.Wizard;
51 import org.eclipse.ui.IEditorPart;
52 import org.eclipse.ui.INewWizard;
53 import org.eclipse.ui.IWorkbench;
54 import org.eclipse.ui.PartInitException;
55 
56 import java.io.ByteArrayInputStream;
57 import java.io.InputStream;
58 import java.io.UnsupportedEncodingException;
59 
60 /**
61  * The "New Android XML File Wizard" provides the ability to create skeleton XML
62  * resources files for Android projects.
63  * <p/>
64  * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project,
65  * the resource folder, resource type and file name. It then creates the XML file.
66  */
67 public class NewXmlFileWizard extends Wizard implements INewWizard {
68     /** The XML header to write at the top of the XML file */
69     public static final String XML_HEADER_LINE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$
70 
71     private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$
72 
73     protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$
74 
75     private NewXmlFileCreationPage mMainPage;
76     private ChooseConfigurationPage mConfigPage;
77     private Values mValues;
78 
79     @Override
init(IWorkbench workbench, IStructuredSelection selection)80     public void init(IWorkbench workbench, IStructuredSelection selection) {
81         setHelpAvailable(false); // TODO have help
82         setWindowTitle("New Android XML File");
83         setImageDescriptor();
84 
85         mValues = new Values();
86         mMainPage = createMainPage(mValues);
87         mMainPage.setTitle("New Android XML File");
88         mMainPage.setDescription("Creates a new Android XML file.");
89         mMainPage.setInitialSelection(selection);
90 
91         mConfigPage = new ChooseConfigurationPage(mValues);
92 
93         // Trigger a check to see if the SDK needs to be reloaded (which will
94         // invoke onSdkLoaded asynchronously as needed).
95         AdtPlugin.getDefault().refreshSdk();
96     }
97 
98     /**
99      * Creates the wizard page.
100      * <p/>
101      * Please do NOT override this method.
102      * <p/>
103      * This is protected so that it can be overridden by unit tests.
104      * However the contract of this class is private and NO ATTEMPT will be made
105      * to maintain compatibility between different versions of the plugin.
106      */
createMainPage(NewXmlFileWizard.Values values)107     protected NewXmlFileCreationPage createMainPage(NewXmlFileWizard.Values values) {
108         return new NewXmlFileCreationPage(MAIN_PAGE_NAME, values);
109     }
110 
111     // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
112     //
113     // The Wizard class implements most defaults and boilerplate code needed by
114     // IWizard
115 
116     /**
117      * Adds pages to this wizard.
118      */
119     @Override
addPages()120     public void addPages() {
121         addPage(mMainPage);
122         addPage(mConfigPage);
123 
124     }
125 
126     /**
127      * Performs any actions appropriate in response to the user having pressed
128      * the Finish button, or refuse if finishing now is not permitted: here, it
129      * actually creates the workspace project and then switch to the Java
130      * perspective.
131      *
132      * @return True
133      */
134     @Override
performFinish()135     public boolean performFinish() {
136         final Pair<IFile, IRegion> created = createXmlFile();
137         if (created == null) {
138             return false;
139         } else {
140             // Open the file
141             // This has to be delayed in order for focus handling to work correctly
142             AdtPlugin.getDisplay().asyncExec(new Runnable() {
143                 @Override
144                 public void run() {
145                     IFile file = created.getFirst();
146                     IRegion region = created.getSecond();
147                     try {
148                         IEditorPart editor = AdtPlugin.openFile(file, null,
149                                 false /*showEditorTab*/);
150                         if (editor instanceof AndroidXmlEditor) {
151                             final AndroidXmlEditor xmlEditor = (AndroidXmlEditor)editor;
152                             if (!xmlEditor.hasMultiplePages()) {
153                                 xmlEditor.show(region.getOffset(), region.getLength(),
154                                         true /* showEditorTab */);
155                             }
156                         }
157                     } catch (PartInitException e) {
158                         AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$
159                                 file.getFullPath().toString());
160                     }
161                 }});
162 
163             return true;
164         }
165     }
166 
167     // -- Custom Methods --
168 
createXmlFile()169     private Pair<IFile, IRegion> createXmlFile() {
170         IFile file = mValues.getDestinationFile();
171         TypeInfo type = mValues.type;
172         if (type == null) {
173             // this is not expected to happen
174             String name = file.getFullPath().toString();
175             AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name);  //$NON-NLS-1$
176             return null;
177         }
178         String xmlns = type.getXmlns();
179         String root = mMainPage.getRootElement();
180         if (root == null) {
181             // this is not expected to happen
182             AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$
183                     file.toString());
184             return null;
185         }
186 
187         String attrs = type.getDefaultAttrs(mValues.project, root);
188         String child = type.getChild(mValues.project, root);
189         return createXmlFile(file, xmlns, root, attrs, child, type.getResFolderType());
190     }
191 
192     /** Creates a new file using the given root element, namespace and root attributes */
createXmlFile(IFile file, String xmlns, String root, String rootAttributes, String child, ResourceFolderType folderType)193     private static Pair<IFile, IRegion> createXmlFile(IFile file, String xmlns,
194             String root, String rootAttributes, String child, ResourceFolderType folderType) {
195         String name = file.getFullPath().toString();
196         boolean need_delete = false;
197 
198         if (file.exists()) {
199             if (!AdtPlugin.displayPrompt("New Android XML File",
200                 String.format("Do you want to overwrite the file %1$s ?", name))) {
201                 // abort if user selects cancel.
202                 return null;
203             }
204             need_delete = true;
205         } else {
206             AdtUtils.createWsParentDirectory(file.getParent());
207         }
208 
209         StringBuilder sb = new StringBuilder(XML_HEADER_LINE);
210 
211         if (folderType == ResourceFolderType.LAYOUT && root.equals(GRID_LAYOUT)) {
212             IProject project = file.getParent().getProject();
213             int minSdk = ManifestInfo.get(project).getMinSdkVersion();
214             if (minSdk < 14) {
215                 root = SupportLibraryHelper.getTagFor(project, FQCN_GRID_LAYOUT);
216                 if (root.equals(FQCN_GRID_LAYOUT)) {
217                     root = GRID_LAYOUT;
218                 }
219             }
220         }
221 
222         sb.append('<').append(root);
223         if (xmlns != null) {
224             sb.append('\n').append("  xmlns:android=\"").append(xmlns).append('"');  //$NON-NLS-1$
225         }
226 
227         if (rootAttributes != null) {
228             sb.append("\n  ");                       //$NON-NLS-1$
229             sb.append(rootAttributes.replace("\n", "\n  "));  //$NON-NLS-1$ //$NON-NLS-2$
230         }
231 
232         sb.append(">\n");                            //$NON-NLS-1$
233 
234         if (child != null) {
235             sb.append(child);
236         }
237 
238         boolean autoFormat = AdtPrefs.getPrefs().getUseCustomXmlFormatter();
239 
240         // Insert an indented caret. Since the markup here will be reformatted, we need to
241         // insert text tokens that the formatter will preserve, which we can then turn back
242         // into indentation and a caret offset:
243         final String indentToken = "${indent}"; //$NON-NLS-1$
244         final String caretToken = "${caret}";   //$NON-NLS-1$
245         sb.append(indentToken);
246         sb.append(caretToken);
247         if (!autoFormat) {
248             sb.append('\n');
249         }
250 
251         sb.append("</").append(root).append(">\n");  //$NON-NLS-1$ //$NON-NLS-2$
252 
253         EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
254         String fileContents;
255         if (!autoFormat) {
256             fileContents = sb.toString();
257         } else {
258             XmlFormatStyle style = EclipseXmlPrettyPrinter.getForFolderType(folderType);
259             fileContents = EclipseXmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs,
260                     style, null /*lineSeparator*/);
261         }
262 
263         // Remove marker tokens and replace them with whitespace
264         fileContents = fileContents.replace(indentToken, formatPrefs.getOneIndentUnit());
265         int caretOffset = fileContents.indexOf(caretToken);
266         if (caretOffset != -1) {
267             fileContents = fileContents.replace(caretToken, ""); //$NON-NLS-1$
268         }
269 
270         String error = null;
271         try {
272             byte[] buf = fileContents.getBytes("UTF8");    //$NON-NLS-1$
273             InputStream stream = new ByteArrayInputStream(buf);
274             if (need_delete) {
275                 file.delete(IResource.KEEP_HISTORY | IResource.FORCE, null /*monitor*/);
276             }
277             file.create(stream, true /*force*/, null /*progress*/);
278             IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null;
279 
280             // If you introduced a new locale, or new screen variations etc, ensure that
281             // the list of render previews is updated if necessary
282             if (file.getParent().getName().indexOf('-') != -1
283                     && (folderType == ResourceFolderType.LAYOUT
284                         || folderType == ResourceFolderType.VALUES)) {
285                 RenderPreviewManager.bumpRevision();
286             }
287 
288             return Pair.of(file, region);
289         } catch (UnsupportedEncodingException e) {
290             error = e.getMessage();
291         } catch (CoreException e) {
292             error = e.getMessage();
293         }
294 
295         error = String.format("Failed to generate %1$s: %2$s", name, error);
296         AdtPlugin.displayError("New Android XML File", error);
297         return null;
298     }
299 
300     /**
301      * Returns true if the New XML Wizard can create new files of the given
302      * {@link ResourceFolderType}
303      *
304      * @param folderType the folder type to create a file for
305      * @return true if this wizard can create new files for the given folder type
306      */
canCreateXmlFile(ResourceFolderType folderType)307     public static boolean canCreateXmlFile(ResourceFolderType folderType) {
308         TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType);
309         return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null ||
310                 typeInfo.getRootSeed() instanceof String);
311     }
312 
313     /**
314      * Creates a new XML file using the template according to the given folder type
315      *
316      * @param project the project to create the file in
317      * @param file the file to be created
318      * @param folderType the type of folder to look up a template for
319      * @return the created file
320      */
createXmlFile(IProject project, IFile file, ResourceFolderType folderType)321     public static Pair<IFile, IRegion> createXmlFile(IProject project, IFile file,
322             ResourceFolderType folderType) {
323         TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType);
324         String xmlns = type.getXmlns();
325         String root = type.getDefaultRoot(project);
326         if (root == null) {
327             root = type.getRootSeed().toString();
328         }
329         String attrs = type.getDefaultAttrs(project, root);
330         return createXmlFile(file, xmlns, root, attrs, null, folderType);
331     }
332 
333     /**
334      * Returns an image descriptor for the wizard logo.
335      */
setImageDescriptor()336     private void setImageDescriptor() {
337         ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
338         setDefaultPageImageDescriptor(desc);
339     }
340 
341     /**
342      * Specific New XML File wizard tied to the {@link ResourceFolderType#LAYOUT} type
343      */
344     public static class NewLayoutWizard extends NewXmlFileWizard {
345         /** Creates a new {@link NewLayoutWizard} */
NewLayoutWizard()346         public NewLayoutWizard() {
347         }
348 
349         @Override
init(IWorkbench workbench, IStructuredSelection selection)350         public void init(IWorkbench workbench, IStructuredSelection selection) {
351             super.init(workbench, selection);
352             setWindowTitle("New Android Layout XML File");
353             super.mMainPage.setTitle("New Android Layout XML File");
354             super.mMainPage.setDescription("Creates a new Android Layout XML file.");
355             super.mMainPage.setInitialFolderType(ResourceFolderType.LAYOUT);
356         }
357     }
358 
359     /**
360      * Specific New XML File wizard tied to the {@link ResourceFolderType#VALUES} type
361      */
362     public static class NewValuesWizard extends NewXmlFileWizard {
363         /** Creates a new {@link NewValuesWizard} */
NewValuesWizard()364         public NewValuesWizard() {
365         }
366 
367         @Override
init(IWorkbench workbench, IStructuredSelection selection)368         public void init(IWorkbench workbench, IStructuredSelection selection) {
369             super.init(workbench, selection);
370             setWindowTitle("New Android Values XML File");
371             super.mMainPage.setTitle("New Android Values XML File");
372             super.mMainPage.setDescription("Creates a new Android Values XML file.");
373             super.mMainPage.setInitialFolderType(ResourceFolderType.VALUES);
374         }
375     }
376 
377     /** Value object which holds the current state of the wizard pages */
378     public static class Values {
379         /** The currently selected project, or null */
380         public IProject project;
381         /** The root name of the XML file to create, or null */
382         public String name;
383         /** The type of XML file to create */
384         public TypeInfo type;
385         /** The path within the project to create the new file in */
386         public String folderPath;
387         /** The currently chosen configuration */
388         public FolderConfiguration configuration = new FolderConfiguration();
389 
390         /**
391          * Returns the destination filename or an empty string.
392          *
393          * @return the filename, never null.
394          */
getFileName()395         public String getFileName() {
396             String fileName;
397             if (name == null) {
398                 fileName = ""; //$NON-NLS-1$
399             } else {
400                 fileName = name.trim();
401                 if (fileName.length() > 0 && fileName.indexOf('.') == -1) {
402                     fileName = fileName + SdkConstants.DOT_XML;
403                 }
404             }
405 
406             return fileName;
407         }
408 
409         /**
410          * Returns a {@link IFile} for the destination file.
411          * <p/>
412          * Returns null if the project, filename or folder are invalid and the
413          * destination file cannot be determined.
414          * <p/>
415          * The {@link IFile} is a resource. There might or might not be an
416          * actual real file.
417          *
418          * @return an {@link IFile} for the destination file
419          */
getDestinationFile()420         public IFile getDestinationFile() {
421             String fileName = getFileName();
422             if (project != null && folderPath != null && folderPath.length() > 0
423                     && fileName.length() > 0) {
424                 IPath dest = new Path(folderPath).append(fileName);
425                 IFile file = project.getFile(dest);
426                 return file;
427             }
428             return null;
429         }
430     }
431 }
432