/******************************************************************************* * Copyright (c) 2016 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.dash.metadata; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.AclEntryPermission; import java.nio.file.attribute.AclEntryType; import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.UserPrincipal; import java.nio.file.attribute.UserPrincipalLookupService; import java.util.Collections; import java.util.EnumSet; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.springframework.ide.eclipse.boot.util.Log; import org.springsource.ide.eclipse.commons.core.util.OsUtils; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; /** * Implementation of IPropertyStore backed by a .properties file. * The properties file is 'private'. I.e. it is created such that only * the onwer of the file is allowed access to it. */ public class PropertyFileStore implements IPropertyStore { final private File file; final private Properties props = new Properties(); Job flushProperties = new Job("Save private properties") { boolean errorLogged = false; { setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { try { createFileIfNeeded(file); try (OutputStream out = new FileOutputStream(file)) { props.store(out, "Private properties"); } } catch (IOException e) { if (!errorLogged) { errorLogged = true; //complaining once about failure to store props is enough! Log.log(e); } } return Status.OK_STATUS; } }; protected void createFileIfNeeded(File f) throws IOException { if (!f.exists()) { createFile(f); setPermissions(f); } } protected void setPermissions(File f) throws IOException { if (OsUtils.isWindows()) { setWindowsPerissions(f); } else { setUnixPermissions(f); } } protected void setWindowsPerissions(File f) throws IOException { Path file = f.toPath(); AclFileAttributeView aclAttr = Files.getFileAttributeView(file, AclFileAttributeView.class); UserPrincipalLookupService upls = file.getFileSystem().getUserPrincipalLookupService(); UserPrincipal user = upls.lookupPrincipalByName(System.getProperty("user.name")); AclEntry.Builder builder = AclEntry.newBuilder(); builder.setPermissions( EnumSet.allOf(AclEntryPermission.class)); // This was here before but it seems not enough to actually allow reading the file: // builder.setPermissions( EnumSet.of( // AclEntryPermission.READ_DATA, // AclEntryPermission.WRITE_DATA, // AclEntryPermission.APPEND_DATA, // AclEntryPermission.READ_ACL, // AclEntryPermission.WRITE_ACL, // AclEntryPermission.READ_ATTRIBUTES, // AclEntryPermission.WRITE_ATTRIBUTES, // AclEntryPermission.READ_NAMED_ATTRS, // AclEntryPermission.WRITE_NAMED_ATTRS, // AclEntryPermission.DELETE // )); builder.setPrincipal(user); builder.setType(AclEntryType.ALLOW); aclAttr.setAcl(Collections.singletonList(builder.build())); } protected void setUnixPermissions(File f) throws IOException { Files.setPosixFilePermissions(file.toPath(), EnumSet.of( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE) ); } protected void createFile(File f) throws IOException { if (f.getParentFile() != null) { f.getParentFile().mkdirs(); } f.createNewFile(); } public PropertyFileStore(File file) { this.file = file; try { if (file.exists()) { try (InputStream in = new FileInputStream(file)) { props.load(in); } } } catch (IOException e) { //Log and move on. This is not 'critical'. Maybe the file is corrupt and will be overwritten // or something else is wrong and we won't be able to persist props. But at least //props will be working in memory. Log.log(e); } } @Override public synchronized String get(String key) { return props.getProperty(key); } @Override public void put(String key, String value) throws Exception { Object oldValue = nullPut(key, value); if (!Objects.equals(oldValue, value)) { flushProperties.schedule(); } } private Object nullPut(String key, String value) { if (value==null) { return props.remove(key); } else { return props.put(key, value); } } public boolean isEmpty() { return props.isEmpty(); } /** * Blocks the current thread until all dirty data has been written to disk. This meant for testing purposes. * E.g. a test that wants to check contents of the backing file after performing ops on the store. */ public void sync() throws InterruptedException { flushProperties.join(); } /** * Convert the contents currently in the store into a Map. Mainly meant for testing purposes to easily compare * the map contents to expected contents. Not thread safe. Test code is expected to call this at a time * when the work on the map is 'done'. */ public Map<String,String> asMap() { Builder<String, String> builder = ImmutableMap.builder(); for (Entry<Object, Object> e : props.entrySet()) { builder.put((String)e.getKey(), (String)e.getValue()); } return builder.build(); } }