/******************************************************************************* * Copyright (c) 2015 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.properties.editor.quickfix; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.LinkedHashMap; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.text.Document; import org.springframework.ide.eclipse.org.json.JSONArray; import org.springframework.ide.eclipse.org.json.JSONObject; import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil; /** * Helper class to manipulate data in a file presumed to contain * spring-boot configuration data. * * @author Kris De Volder */ public class MetaDataManipulator { private abstract class Content { public abstract String toString(); public abstract void addProperty(JSONObject jsonObject) throws Exception; } /** * Content was parse as JSONObject. */ private class ParsedContent extends Content { private JSONObject object; public ParsedContent(JSONObject o) { this.object = o; } public String toString() { return object.toString(indentFactor); } @Override public void addProperty(JSONObject propertyData) throws Exception { JSONArray properties = object.getJSONArray("properties"); properties.put(properties.length(), propertyData); } } /** * Content that is 'unparsed' and just a bunch of text. * Used only as a fallback when data in file can't * be parsed. * <p> * This content is manipulated by string manipulation. * It is less reliable, but can be done even if the * file data is not parseable. */ private class RawContent extends Content { private Document doc; public RawContent(Document doc) { this.doc = doc; } @Override public String toString() { return doc.get(); } @Override public void addProperty(JSONObject propertyData) throws Exception { String newline = getNewline(); int insertAt = findLast(']'); if (insertAt<0) { //although we're not looking for much, we didn't find it! //Funky file contents. Let's just insert something at end of file in a 'best effort' spirit. insertAt = doc.getLength(); } insert(insertAt, newline); insert(insertAt, propertyData.toString(indentFactor)); int insertComma = findInsertCommaPos(insertAt); if (insertComma>=0) { insert(insertComma, ","); } } /** * Maybe we need to add a comma in front of the new entry. This * method finds if/where to stick this comma. * @throws Exception */ private int findInsertCommaPos(int pos) throws Exception { pos--; Document d = doc; while (pos>=0 && Character.isWhitespace(d.getChar(pos))) { pos--; } if (pos>=0) { char c = d.getChar(pos); if (c == '}') { //Add a comma after a '}' return pos+1; } } return -1; } private int insert(int insertAt, String str) throws Exception { doc.replace(insertAt, 0, str); return insertAt + str.length(); } private int findLast(char toFind) throws Exception { Document d = doc; int pos = d.getLength()-1; while (pos>=0 && d.getChar(pos)!=toFind) { pos--; } //We got here either because // - we found char at pos or.. // - we reached position *before* start of file (i.e. -1) return pos; } private String getNewline() throws Exception { if (fNewline==null) { fNewline = doc.getDefaultLineDelimiter().toString(); } return fNewline; } } public interface ContentStore { InputStream getContents() throws Exception; void setContents(InputStream inputStream) throws Exception; } private static final String INITIAL_CONTENT = "{\"properties\": [\n" + "]}"; private static String ENCODING = "UTF8"; private ContentStore file; private Content fContent; private String fNewline; private int indentFactor = 2; public MetaDataManipulator(final IFile file) { this(new ContentStore() { public InputStream getContents() throws Exception { return file.getContents(); } @Override public void setContents(InputStream inputStream) throws Exception { file.setContents(inputStream, true, true, new NullProgressMonitor()); } }); } public MetaDataManipulator(ContentStore contentStore) { this.file = contentStore; } private Content getContent() throws Exception { if (fContent==null) { fContent = readContent(file, ENCODING); } return fContent; } private Content readContent(ContentStore file, String encoding) throws Exception { Document d = readDocument(file, encoding); if (isEmpty(d)) { JSONObject o = initialContent(); return new ParsedContent(o); } else { try { return new ParsedContent(new JSONObject(d.get())); } catch (Exception e) { //couldn't parse? return new RawContent(d); } } } private Document readDocument(ContentStore file, String encoding) throws Exception { InputStream data = file.getContents(); try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); IOUtil.pipe(data, bytes); return new Document(new String(bytes.toByteArray(), encoding)); } finally { data.close(); } } public void addDefaultInfo(String propertyName) throws Exception { getContent().addProperty(createDefaultData(propertyName)); } private JSONObject createDefaultData(String propertyName) throws Exception { JSONObject obj = new JSONObject(new LinkedHashMap<String, Object>()); obj.put("name", propertyName); obj.put("type", String.class.getName()); obj.put("description", "A description for '"+propertyName+"'"); return obj; } /** * Generate the initial content (must be generated rather than being a constant to respect newline conventions * on user's system. */ private JSONObject initialContent() throws Exception { return new JSONObject(INITIAL_CONTENT); } /** * Checks if file 'looks empty'. Files with only whitespace in them * are considered empty. */ private boolean isEmpty(Document d) throws Exception { int len = d.getLength(); for (int i = 0; i < len; i++) { if (!Character.isWhitespace(d.getChar(i))) { return false; } } return true; } /** * After manipulating the data, use this to persist changes back to the file. */ public void save() throws Exception { file.setContents(toInputStream(getContent())); } private InputStream toInputStream(Content content) throws Exception { return new ByteArrayInputStream(content.toString().getBytes(ENCODING)); } /** * Determines whether the 'reliable' manipulations can be used (which is the case * only if the data in the file is valid json). */ public boolean isReliable() throws Exception { return getContent() instanceof ParsedContent; } }