/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.components.impl.stores;
import com.intellij.notification.NotificationType;
import com.intellij.notification.NotificationsManager;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.StateStorage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
import com.intellij.openapi.components.store.ReadOnlyModificationException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectBundle;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.util.LineSeparator;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Parent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.LinkedHashSet;
import java.util.List;
/**
* @author mike
*/
public class StorageUtil {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.components.impl.stores.StorageUtil");
private static final byte[] XML_PROLOG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".getBytes(CharsetToolkit.UTF8_CHARSET);
@SuppressWarnings("SpellCheckingInspection")
private static final Pair<byte[], String> NON_EXISTENT_FILE_DATA = Pair.create(null, SystemProperties.getLineSeparator());
private StorageUtil() { }
public static boolean isChangedByStorageOrSaveSession(@NotNull VirtualFileEvent event) {
return event.getRequestor() instanceof StateStorage.SaveSession || event.getRequestor() instanceof StateStorage;
}
public static void notifyUnknownMacros(@NotNull TrackingPathMacroSubstitutor substitutor,
@NotNull final Project project,
@Nullable final String componentName) {
final LinkedHashSet<String> macros = new LinkedHashSet<String>(substitutor.getUnknownMacros(componentName));
if (macros.isEmpty()) {
return;
}
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
macros.removeAll(getMacrosFromExistingNotifications(project));
if (!macros.isEmpty()) {
LOG.debug("Reporting unknown path macros " + macros + " in component " + componentName);
String format = "<p><i>%s</i> %s undefined. <a href=\"define\">Fix it</a></p>";
String productName = ApplicationNamesInfo.getInstance().getProductName();
String content = String.format(format, StringUtil.join(macros, ", "), macros.size() == 1 ? "is" : "are") +
"<br>Path variables are used to substitute absolute paths " +
"in " + productName + " project files " +
"and allow project file sharing in version control systems.<br>" +
"Some of the files describing the current project settings contain unknown path variables " +
"and " + productName + " cannot restore those paths.";
new UnknownMacroNotification("Load Error", "Load error: undefined path variables", content, NotificationType.ERROR,
(notification, event) -> ((ProjectEx)project).checkUnknownMacros(true), macros).notify(project);
}
}
});
}
private static List<String> getMacrosFromExistingNotifications(Project project) {
List<String> notified = ContainerUtil.newArrayList();
NotificationsManager manager = NotificationsManager.getNotificationsManager();
for (final UnknownMacroNotification notification : manager.getNotificationsOfType(UnknownMacroNotification.class, project)) {
notified.addAll(notification.getMacros());
}
return notified;
}
public static boolean isEmpty(@Nullable Parent element) {
if (element == null) {
return true;
}
else if (element instanceof Element) {
return JDOMUtil.isEmpty((Element)element);
}
else {
Document document = (Document)element;
return !document.hasRootElement() || JDOMUtil.isEmpty(document.getRootElement());
}
}
@NotNull
public static VirtualFile writeFile(@Nullable File file, @NotNull Object requestor, @Nullable VirtualFile virtualFile, @NotNull BufferExposingByteArrayOutputStream content, @Nullable LineSeparator lineSeparatorIfPrependXmlProlog) throws IOException {
AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(StorageUtil.class);
try {
if (file != null && (virtualFile == null || !virtualFile.isValid())) {
virtualFile = getOrCreateVirtualFile(requestor, file);
}
assert virtualFile != null;
OutputStream out = virtualFile.getOutputStream(requestor);
try {
if (lineSeparatorIfPrependXmlProlog != null) {
out.write(XML_PROLOG);
out.write(lineSeparatorIfPrependXmlProlog.getSeparatorBytes());
}
content.writeTo(out);
}
finally {
out.close();
}
return virtualFile;
}
catch (FileNotFoundException e) {
if (virtualFile == null) {
throw e;
}
else {
throw new ReadOnlyModificationException(virtualFile);
}
}
finally {
token.finish();
}
}
public static void deleteFile(@NotNull File file, @NotNull Object requestor, @Nullable VirtualFile virtualFile) throws IOException {
if (virtualFile == null) {
LOG.warn("Cannot find virtual file " + file.getAbsolutePath());
}
if (virtualFile == null) {
if (file.exists()) {
FileUtil.delete(file);
}
}
else if (virtualFile.exists()) {
deleteFile(requestor, virtualFile);
}
}
public static void deleteFile(@NotNull Object requestor, @NotNull VirtualFile virtualFile) throws IOException {
AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(StorageUtil.class);
try {
virtualFile.delete(requestor);
}
catch (FileNotFoundException e) {
throw new ReadOnlyModificationException(virtualFile);
}
finally {
token.finish();
}
}
@NotNull
public static BufferExposingByteArrayOutputStream writeToBytes(@NotNull Parent element, @NotNull String lineSeparator) throws IOException {
BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream(512);
JDOMUtil.writeParent(element, out, lineSeparator);
return out;
}
@NotNull
private static VirtualFile getOrCreateVirtualFile(@Nullable Object requestor, @NotNull File ioFile) throws IOException {
VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioFile);
if (virtualFile == null) {
File parentFile = ioFile.getParentFile();
// need refresh if the directory has just been created
VirtualFile parentVirtualFile = parentFile == null ? null : LocalFileSystem.getInstance().refreshAndFindFileByIoFile(parentFile);
if (parentVirtualFile == null) {
throw new IOException(ProjectBundle.message("project.configuration.save.file.not.found", parentFile == null ? ioFile.getPath() : parentFile.getPath()));
}
virtualFile = parentVirtualFile.createChildData(requestor, ioFile.getName());
}
return virtualFile;
}
/**
* @return pair.first - file contents (null if file does not exist), pair.second - file line separators
*/
@NotNull
public static Pair<byte[], String> loadFile(@Nullable final VirtualFile file) throws IOException {
if (file == null || !file.exists()) {
return NON_EXISTENT_FILE_DATA;
}
byte[] bytes = file.contentsToByteArray();
String lineSeparator = file.getDetectedLineSeparator();
if (lineSeparator == null) {
lineSeparator = detectLineSeparators(CharsetToolkit.UTF8_CHARSET.decode(ByteBuffer.wrap(bytes)), null).getSeparatorString();
}
return Pair.create(bytes, lineSeparator);
}
@NotNull
public static LineSeparator detectLineSeparators(@NotNull CharSequence chars, @Nullable LineSeparator defaultSeparator) {
for (int i = 0, n = chars.length(); i < n; i++) {
char c = chars.charAt(i);
if (c == '\r') {
return LineSeparator.CRLF;
}
else if (c == '\n') {
// if we are here, there was no \r before
return LineSeparator.LF;
}
}
return defaultSeparator == null ? LineSeparator.getSystemLineSeparator() : defaultSeparator;
}
@Nullable
public static BufferExposingByteArrayOutputStream newContentIfDiffers(@NotNull Parent element, @Nullable VirtualFile file) {
try {
Pair<byte[], String> pair = loadFile(file);
BufferExposingByteArrayOutputStream out = writeToBytes(element, pair.second);
return pair.first != null && equal(pair.first, out) ? null : out;
}
catch (IOException e) {
LOG.debug(e);
return null;
}
}
public static boolean equal(byte[] a1, @NotNull BufferExposingByteArrayOutputStream out) {
int length = out.size();
if (a1.length != length) {
return false;
}
byte[] internalBuffer = out.getInternalBuffer();
for (int i = 0; i < length; i++) {
if (a1[i] != internalBuffer[i]) {
return false;
}
}
return true;
}
@Nullable
public static Document loadDocument(final byte[] bytes) {
try {
return bytes == null || bytes.length == 0 ? null : JDOMUtil.loadDocument(new ByteArrayInputStream(bytes));
}
catch (JDOMException e) {
return null;
}
catch (IOException e) {
return null;
}
}
@Nullable
public static Document loadDocument(@Nullable InputStream stream) {
if (stream == null) {
return null;
}
try {
try {
return JDOMUtil.loadDocument(stream);
}
finally {
stream.close();
}
}
catch (JDOMException e) {
return null;
}
catch (IOException e) {
return null;
}
}
@NotNull
public static BufferExposingByteArrayOutputStream elementToBytes(@NotNull Parent element, boolean useSystemLineSeparator) throws IOException {
return writeToBytes(element, useSystemLineSeparator ? SystemProperties.getLineSeparator() : "\n");
}
public static void sendContent(@NotNull StreamProvider provider, @NotNull String fileSpec, @NotNull Parent element, @NotNull RoamingType type) {
if (!provider.isApplicable(fileSpec, type)) {
return;
}
try {
doSendContent(provider, fileSpec, element, type);
}
catch (IOException e) {
LOG.warn(e);
}
}
public static void delete(@NotNull StreamProvider provider, @NotNull String fileSpec, @NotNull RoamingType type) {
if (provider.isApplicable(fileSpec, type)) {
provider.delete(fileSpec, type);
}
}
/**
* You must call {@link StreamProvider#isApplicable(String, com.intellij.openapi.components.RoamingType)} before
*/
public static void doSendContent(@NotNull StreamProvider provider, @NotNull String fileSpec, @NotNull Parent element, @NotNull RoamingType type) throws IOException {
// we should use standard line-separator (\n) - stream provider can share file content on any OS
BufferExposingByteArrayOutputStream content = elementToBytes(element, false);
provider.saveContent(fileSpec, content.getInternalBuffer(), content.size(), type);
}
public static boolean isProjectOrModuleFile(@NotNull String fileSpec) {
return fileSpec.startsWith(StoragePathMacros.PROJECT_CONFIG_DIR);
}
@NotNull
public static String getStoreDir(@NotNull Project project) {
return project.getBasePath() + "/" + Project.DIRECTORY_STORE_FOLDER;
}
}