1/*
2 * Copyright (C) 2010 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
17package com.android.ide.eclipse.adt.internal.editors.export;
18
19import com.android.SdkConstants;
20import com.android.ide.eclipse.adt.AdtPlugin;
21import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
22
23import org.eclipse.jface.text.BadLocationException;
24import org.eclipse.jface.text.DocumentEvent;
25import org.eclipse.jface.text.IDocument;
26import org.eclipse.jface.text.IRegion;
27import org.eclipse.swt.events.ModifyEvent;
28import org.eclipse.swt.events.ModifyListener;
29import org.eclipse.swt.widgets.Composite;
30import org.eclipse.swt.widgets.Control;
31import org.eclipse.swt.widgets.Text;
32import org.eclipse.ui.forms.widgets.FormToolkit;
33import org.eclipse.ui.forms.widgets.Section;
34
35import java.util.HashMap;
36import java.util.HashSet;
37import java.util.Iterator;
38
39/**
40 * Section part for editing fields of a properties file in an Export editor.
41 * <p/>
42 * This base class is intended to be derived and customized.
43 */
44abstract class AbstractPropertiesFieldsPart extends ManifestSectionPart {
45
46    private final HashMap<String, Control> mNameToField = new HashMap<String, Control>();
47
48    private ExportEditor mEditor;
49
50    private boolean mInternalTextUpdate = false;
51
52    public AbstractPropertiesFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
53        super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
54        mEditor = editor;
55    }
56
57    protected HashMap<String, Control> getNameToField() {
58        return mNameToField;
59    }
60
61    protected ExportEditor getEditor() {
62        return mEditor;
63    }
64
65    protected void setInternalTextUpdate(boolean internalTextUpdate) {
66        mInternalTextUpdate = internalTextUpdate;
67    }
68
69    protected boolean isInternalTextUpdate() {
70        return mInternalTextUpdate;
71    }
72
73    /**
74     * Adds a modify listener to every text field that will mark the part as dirty.
75     *
76     * CONTRACT: Derived classes MUST call this at the end of their constructor.
77     *
78     * @see #setFieldModifyListener(Control, ModifyListener)
79     */
80    protected void addModifyListenerToFields() {
81        ModifyListener markDirtyListener = new ModifyListener() {
82            @Override
83            public void modifyText(ModifyEvent e) {
84                // Mark the part as dirty if a field has been changed.
85                // This will force a commit() operation to store the data in the model.
86                if (!mInternalTextUpdate) {
87                    markDirty();
88                }
89            }
90        };
91
92        for (Control field : mNameToField.values()) {
93            setFieldModifyListener(field, markDirtyListener);
94        }
95    }
96
97    /**
98     * Sets a listener that will mark the part as dirty when the control is modified.
99     * The base method only handles {@link Text} fields.
100     *
101     * CONTRACT: Derived classes CAN use this to add a listener to their own controls.
102     * The listener must call {@link #markDirty()} when the control is modified by the user.
103     *
104     * @param field A control previously registered with {@link #getNameToField()}.
105     * @param markDirtyListener A {@link ModifyListener} that invokes {@link #markDirty()}.
106     *
107     * @see #isInternalTextUpdate()
108     */
109    protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) {
110        if (field instanceof Text) {
111            ((Text) field).addModifyListener(markDirtyListener);
112        }
113    }
114
115    /**
116     * Updates the model based on the content of fields. This is invoked when a field
117     * has marked the document as dirty.
118     *
119     * CONTRACT: Derived classes do not need to override this.
120     */
121    @Override
122    public void commit(boolean onSave) {
123
124        // We didn't store any information indicating which field was dirty (we could).
125        // Since there are not many fields, just update all the document lines that
126        // match our field keywords.
127
128        if (isDirty()) {
129            mEditor.wrapRewriteSession(new Runnable() {
130                @Override
131                public void run() {
132                    saveFieldsToModel();
133                }
134            });
135        }
136
137        super.commit(onSave);
138    }
139
140    private void saveFieldsToModel() {
141        // Get a list of all keywords to process. Go thru the document, replacing in-place
142        // the ones we can find and remove them from this set. This will leave the list
143        // of new keywords to add at the end of the document.
144        HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet());
145
146        IDocument doc = mEditor.getDocument();
147        int numLines = doc.getNumberOfLines();
148
149        String delim = null;
150        try {
151            delim = numLines > 0 ? doc.getLineDelimiter(0) : null;
152        } catch (BadLocationException e1) {
153            // ignore
154        }
155        if (delim == null || delim.length() == 0) {
156            delim = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS ?
157                    "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2#
158        }
159
160        for (int i = 0; i < numLines; i++) {
161            try {
162                IRegion info = doc.getLineInformation(i);
163                String line = doc.get(info.getOffset(), info.getLength());
164                line = line.trim();
165                if (line.startsWith("#")) {  //$NON-NLS-1$
166                    continue;
167                }
168
169                int pos = line.indexOf('=');
170                if (pos > 0 && pos < line.length() - 1) {
171                    String key = line.substring(0, pos).trim();
172
173                    Control field = mNameToField.get(key);
174                    if (field != null) {
175
176                        // This is the new line to inject
177                        line = key + "=" + getFieldText(field);
178
179                        try {
180                            // replace old line by new one. This doesn't change the
181                            // line delimiter.
182                            mInternalTextUpdate = true;
183                            doc.replace(info.getOffset(), info.getLength(), line);
184                            allKeywords.remove(key);
185                        } finally {
186                            mInternalTextUpdate = false;
187                        }
188                    }
189                }
190
191            } catch (BadLocationException e) {
192                // TODO log it
193                AdtPlugin.log(e, "Failed to replace in export.properties");
194            }
195        }
196
197        for (String key : allKeywords) {
198            Control field = mNameToField.get(key);
199            if (field != null) {
200                // This is the new line to inject
201                String line = key + "=" + getFieldText(field);
202
203                try {
204                    // replace old line by new one
205                    mInternalTextUpdate = true;
206
207                    numLines = doc.getNumberOfLines();
208
209                    IRegion info = numLines > 0 ? doc.getLineInformation(numLines - 1) : null;
210                    if (info != null && info.getLength() == 0) {
211                        // last line is empty. Insert right before there.
212                        doc.replace(info.getOffset(), info.getLength(), line);
213                    } else {
214                        if (numLines > 0) {
215                            String eofDelim = doc.getLineDelimiter(numLines - 1);
216                            if (eofDelim == null || eofDelim.length() == 0) {
217                                // The document doesn't end with a line delimiter, so add
218                                // one to the line to be written.
219                                line = delim + line;
220                            }
221                        }
222
223                        int len = doc.getLength();
224                        doc.replace(len, 0, line);
225                    }
226
227                    allKeywords.remove(key);
228                } catch (BadLocationException e) {
229                    // TODO log it
230                    AdtPlugin.log(e, "Failed to append to export.properties: %s", line);
231                } finally {
232                    mInternalTextUpdate = false;
233                }
234            }
235        }
236    }
237
238    /**
239     * Used when committing fields values to the model to retrieve the text
240     * associated with a field.
241     * <p/>
242     * The base method only handles {@link Text} controls.
243     *
244     * CONTRACT: Derived classes CAN use this to support their own controls.
245     *
246     * @param field A control previously registered with {@link #getNameToField()}.
247     * @return A non-null string to write to the properties files.
248     */
249    protected String getFieldText(Control field) {
250        if (field instanceof Text) {
251            return ((Text) field).getText();
252        }
253        return "";
254    }
255
256    /**
257     * Called after all pages have been created, to let the parts initialize their
258     * content based on the document's model.
259     * <p/>
260     * The model should be acceded via the {@link ExportEditor}.
261     *
262     * @param editor The {@link ExportEditor} instance.
263     */
264    public void onModelInit(ExportEditor editor) {
265
266        // Start with a set of all the possible keywords and remove those we
267        // found in the document as we read the lines.
268        HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet());
269
270        // Parse the lines in the document for patterns "keyword=value",
271        // trimming all whitespace and discarding lines that start with # (comments)
272        // then affect to the internal fields as appropriate.
273        IDocument doc = editor.getDocument();
274        int numLines = doc.getNumberOfLines();
275        for (int i = 0; i < numLines; i++) {
276            try {
277                IRegion info = doc.getLineInformation(i);
278                String line = doc.get(info.getOffset(), info.getLength());
279                line = line.trim();
280                if (line.startsWith("#")) {  //$NON-NLS-1$
281                    continue;
282                }
283
284                int pos = line.indexOf('=');
285                if (pos > 0 && pos < line.length() - 1) {
286                    String key = line.substring(0, pos).trim();
287
288                    Control field = mNameToField.get(key);
289                    if (field != null) {
290                        String value = line.substring(pos + 1).trim();
291                        try {
292                            mInternalTextUpdate = true;
293                            setFieldText(field, value);
294                            allKeywords.remove(key);
295                        } finally {
296                            mInternalTextUpdate = false;
297                        }
298                    }
299                }
300
301            } catch (BadLocationException e) {
302                // TODO log it
303                AdtPlugin.log(e, "Failed to set field to export.properties value");
304            }
305        }
306
307        // Clear the text of any keyword we didn't find in the document
308        Iterator<String> iterator = allKeywords.iterator();
309        while (iterator.hasNext()) {
310            String key = iterator.next();
311            Control field = mNameToField.get(key);
312            if (field != null) {
313                try {
314                    mInternalTextUpdate = true;
315                    setFieldText(field, "");
316                    iterator.remove();
317                } finally {
318                    mInternalTextUpdate = false;
319                }
320            }
321        }
322    }
323
324    /**
325     * Used when reading the model to set the field values.
326     * <p/>
327     * The base method only handles {@link Text} controls.
328     *
329     * CONTRACT: Derived classes CAN use this to support their own controls.
330     *
331     * @param field A control previously registered with {@link #getNameToField()}.
332     * @param value A non-null string to that was read from the properties files.
333     *              The value is an empty string if the property line is missing.
334     */
335    protected void setFieldText(Control field, String value) {
336        if (field instanceof Text) {
337            ((Text) field).setText(value);
338        }
339    }
340
341    /**
342     * Called after the document model has been changed. The model should be acceded via
343     * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession)
344     *
345     * @param editor The {@link ExportEditor} instance.
346     * @param event Specification of changes applied to document.
347     */
348    public void onModelChanged(ExportEditor editor, DocumentEvent event) {
349        // To simplify and since we don't have many fields, just reload all the values.
350        // A better way would to be to look at DocumentEvent which gives us the offset/length
351        // and text that has changed.
352        if (!mInternalTextUpdate) {
353            onModelInit(editor);
354        }
355    }
356}
357