/* * 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.openapi.components.StateStorageException; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.util.ArrayUtil; import com.intellij.util.SystemProperties; import gnu.trove.THashMap; import gnu.trove.TObjectObjectProcedure; import org.iq80.snappy.SnappyInputStream; import org.iq80.snappy.SnappyOutputStream; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Set; @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") final class StateMap { private static final Logger LOG = Logger.getInstance(StateMap.class); private static final Format XML_FORMAT = Format.getRawFormat(). setTextMode(Format.TextMode.TRIM). setOmitEncoding(true). setOmitDeclaration(true); private final THashMap<String, Object> states; public StateMap() { states = new THashMap<String, Object>(); } public StateMap(StateMap stateMap) { states = new THashMap<String, Object>((Map<String, Object>)stateMap.states); } @NotNull public Set<String> keys() { return states.keySet(); } @NotNull public Collection<Object> values() { return states.values(); } @Nullable public Object get(@NotNull String key) { return states.get(key); } @NotNull public Element getElement(@NotNull String key, @NotNull Map<String, Element> newLiveStates) { Object state = states.get(key); return stateToElement(key, state, newLiveStates); } @NotNull static Element stateToElement(@NotNull String key, @Nullable Object state, @NotNull Map<String, Element> newLiveStates) { if (state instanceof Element) { return ((Element)state).clone(); } else { Element element = newLiveStates.get(key); if (element == null) { assert state != null; element = unarchiveState((byte[])state); } return element; } } public void put(@NotNull String key, @NotNull Object value) { states.put(key, value); } public boolean isEmpty() { return states.isEmpty(); } @Nullable public Element getState(@NotNull String key) { Object state = states.get(key); return state instanceof Element ? (Element)state : null; } public boolean hasState(@NotNull String key) { return states.get(key) instanceof Element; } public boolean hasStates() { if (states.isEmpty()) { return false; } for (Object value : states.values()) { if (value instanceof Element) { return true; } } return false; } public void compare(@NotNull String key, @NotNull StateMap newStates, @NotNull Set<String> diffs) { Object oldState = states.get(key); Object newState = newStates.get(key); if (oldState instanceof Element) { if (!JDOMUtil.areElementsEqual((Element)oldState, (Element)newState)) { diffs.add(key); } } else { assert newState != null; if (getNewByteIfDiffers(key, newState, (byte[])oldState) != null) { diffs.add(key); } } } @Nullable public static byte[] getNewByteIfDiffers(@NotNull String key, @NotNull Object newState, @NotNull byte[] oldState) { byte[] newBytes = newState instanceof Element ? archiveState((Element)newState) : (byte[])newState; if (Arrays.equals(newBytes, oldState)) { return null; } else if (LOG.isDebugEnabled() && SystemProperties.getBooleanProperty("idea.log.changed.components", false)) { String before = stateToString(oldState); String after = stateToString(newState); if (before.equals(after)) { LOG.debug("Serialization error: serialized are different, but unserialized are equal"); } else { LOG.debug(key + " " + StringUtil.repeat("=", 80 - key.length()) + "\nBefore:\n" + before + "\nAfter:\n" + after); } } return newBytes; } @NotNull private static byte[] archiveState(@NotNull Element state) { BufferExposingByteArrayOutputStream byteOut = new BufferExposingByteArrayOutputStream(); try { OutputStreamWriter writer = new OutputStreamWriter(new SnappyOutputStream(byteOut), CharsetToolkit.UTF8_CHARSET); try { XMLOutputter xmlOutputter = new JDOMUtil.MyXMLOutputter(); xmlOutputter.setFormat(XML_FORMAT); xmlOutputter.output(state, writer); } finally { writer.close(); } } catch (IOException e) { throw new StateStorageException(e); } return ArrayUtil.realloc(byteOut.getInternalBuffer(), byteOut.size()); } @Nullable public Element getStateAndArchive(@NotNull String key) { Object state = states.get(key); if (!(state instanceof Element)) { return null; } states.put(key, archiveState((Element)state)); return (Element)state; } @NotNull public static Element unarchiveState(@NotNull byte[] state) { InputStream in = null; try { try { in = new SnappyInputStream(new ByteArrayInputStream(state)); //noinspection ConstantConditions return JDOMUtil.loadDocument(in).detachRootElement(); } finally { if (in != null) { in.close(); } } } catch (IOException e) { throw new StateStorageException(e); } catch (JDOMException e) { throw new StateStorageException(e); } } @NotNull public static String stateToString(@NotNull Object state) { Element element; if (state instanceof Element) { element = (Element)state; } else { try { element = unarchiveState((byte[])state); } catch (Throwable e) { LOG.error(e); return "internal error"; } } return JDOMUtil.writeParent(element, "\n"); } @Nullable public Object remove(@NotNull String key) { return states.remove(key); } public int size() { return states.size(); } public void forEachEntry(@NotNull TObjectObjectProcedure<String, Object> consumer) { states.forEachEntry(consumer); } }