Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
Copyright (c) 2004, 2010 IBM Corporation and others. 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: IBM Corporation - initial API and implementation /
 
 package org.eclipse.osgi.storagemanager;
 
 import java.io.*;
 import java.util.*;
Storage managers provide a facility for tracking the state of a group of files having relationship with each others and being updated by several entities at the same time. The typical usecase is in shared configuration data areas.

The facilities provided here are cooperative. That is, all participants must agree to the conventions and to calling the given API. There is no capacity to enforce these conventions or prohibit corruption.

Clients can not extend this class

Example

 //Open the storage manager
 org.eclipse.osgi.storagemanager.StorageManager cacheStorageManager = new StorageManager("d:/sharedFolder/bar/", false); //$NON-NLS-1$
 try {
  cacheStorageManager.open(true);
 } catch (IOException e) {
 // Ignore the exception. The registry will be rebuilt from source.
 }

 //To read from a file 
 java.io.File fileA = cacheStorageManager.lookup("fileA", false));
 java.io.File fileB = cacheStorageManager.lookup("fileB", false));
 //Do the reading code 
 new java.io.FileOutputStream(fileA);

 //To write in files 
 cacheStorageManager.add("fileC"); //add the file to the filemanager (in this case we assume it is not already here)
 cacheStorageManager.add("fileD");

 // The file is never written directly into the file name, so we create some temporary file
 java.io.File fileC = cacheStorageManager.createTempFile("fileC");
 java.io.File fileD = cacheStorageManager.createTempFile("fileD");

 //Do the actual writing here...

 //Finally update the storagemanager with the actual file to manage. 
 cacheStorageManager.update(new String[] {"fileC", "fileD"}, new String[] {fileC.getName(), fileD.getName()};

 //Close the file manager at the end
 cacheStorageManager.close();
 

Implementation details
The following implementation details are provided to help with understanding the behavior of this class. The general principle is to maintain a table which maps user-level file names onto an actual disk files. If a file needs to be modified, it is stored into a new file. The old content is not removed from disk until all entities have closed there instance of the storage manager. Once the instance has been created, open() must be called before performing any other operation. On open the storage manager obtains a snapshot of the current managed files contents. If an entity updates a managed file, the storage manager will save the content for that instance of the storage manager, all other storage manager instances will still have access to that managed file's content as it was when the instance was first opened.

Since:
3.2
 
 
 // Note the implementation of this class originated from the following deprecated classes: 
 // /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/FileManager.java
 // /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/StreamManager.java
 public final class StorageManager {
 	private static final int FILETYPE_STANDARD = 0;
 	private static final int FILETYPE_RELIABLEFILE = 1;
 	private static final SecureAction secure = (SecureAction) AccessController.doPrivileged(SecureAction.createSecureAction());
 	private static final boolean tempCleanup = Boolean.valueOf(.getProperty("osgi.embedded.cleanTempFiles")).booleanValue(); //$NON-NLS-1$
 	private static final boolean openCleanup = Boolean.valueOf(.getProperty("osgi.embedded.cleanupOnOpen")).booleanValue(); //$NON-NLS-1$
 	private static final String MANAGER_FOLDER = ".manager"//$NON-NLS-1$
 	private static final String TABLE_FILE = ".fileTable"//$NON-NLS-1$
 	private static final String LOCK_FILE = ".fileTableLock"//$NON-NLS-1$
 	private static final int MAX_LOCK_WAIT = 5000; // 5 seconds 
 	// this should be static but the tests expect to be able to create new managers after changing this setting dynamically
 	private final boolean useReliableFiles = Boolean.valueOf(.getProperty("osgi.useReliableFiles")).booleanValue(); //$NON-NLS-1$
 
 	private class Entry {
		int readId;
		int writeId;
		Entry(int readIdint writeIdint type) {
			this. = readId;
			this. = writeId;
			this. = type;
		}
		int getReadId() {
			return ;
		}
		int getWriteId() {
			return ;
		}
		int getFileType() {
			return ;
		}
		void setReadId(int value) {
			 = value;
		}
		void setWriteId(int value) {
			 = value;
		}
		void setFileType(int type) {
			 = type;
		}
	}
	private final File base//The folder managed
	private final File managerRoot//The folder that will contain all the file related to the functionning of the manager (typically a subdir of base)
	private final String lockMode;
	private final File tableFile;
	private final File lockFile// The lock file for the table (this file is the same for all the instances)
	private Locker locker// The locker for the lock
	private File instanceFile//The file representing the running instance. It is created when the table file is read.
	private Locker instanceLocker = null//The locker for the instance file.
	private final boolean readOnly// Whether this storage manager is in read-only mode
	private boolean open// Whether this storage manager is open for use
	// locking related fields
	private int tableStamp = -1;
	private final Properties table = new Properties();

Returns a new storage manager for the area identified by the given base directory.

Parameters:
base the directory holding the files to be managed
lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio and also supports null in which case the lock strategy will be the global one.
	public StorageManager(File baseString lockMode) {
		this(baselockModefalse);
	}

Returns a new storage manager for the area identified by the given base directory.

Parameters:
base the directory holding the files to be managed
lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio and also supports null in which case the lock strategy will be the global one.
readOnly true if the managed files are read-only
	public StorageManager(File baseString lockModeboolean readOnly) {
		this. = base;
		this. = lockMode;
		this. = new File(base);
		this. = readOnly;
		 = false;
	}
	private void initializeInstanceFile() throws IOException {
		if ( != null || )
			return;
		this. = File.createTempFile(".tmp"".instance"); //$NON-NLS-1$//$NON-NLS-2$
	}
	private String getAbsolutePath(String file) {
		return new File(file).getAbsolutePath();
	}

Add the given managed file name to the list of files managed by this manager.

Parameters:
managedFile name of the file to manage
Throws:
java.io.IOException if there are any problems adding the given file name to the manager
	public void add(String managedFilethrows IOException {
		add(managedFile);
	}
	/* (non-Javadoc
	 * Add the given file name to the list of files managed by this manager.
	 * 
	 * @param managedFile name of the file to manage.
	 * @param fileType the file type. 
	 * @throws IOException if there are any problems adding the given file to the manager
	 */
	private void add(String managedFileint fileTypethrows IOException {
		if (!)
		if (!lock(true))
		try {
			Entry entry = (Entry.get(managedFile);
			if (entry == null) {
				entry = new Entry(0, 1, fileType);
				.put(managedFileentry);
				// if this managed file existed before, ensure there is not an old
				// version on the disk to avoid name collisions. If version found,
				// us the oldest generation+1 for the write ID.
				int oldestGeneration = findOldestGeneration(managedFile);
				if (oldestGeneration != 0)
					entry.setWriteId(oldestGeneration + 1);
else {
				if (entry.getFileType() != fileType) {
					entry.setFileType(fileType);
				}
			}
finally {
		}
	}
	/* (non-Javadoc)
	 * Find the oldest generation of a file still available on disk 
	 * @param file the file from which to obtain the oldest generation.
	 * @return the oldest generation of the file or 0 if the file does
	 * not exist. 
	 */
	private int findOldestGeneration(String managedFile) {
		String[] files = .list();
		int oldestGeneration = 0;
		if (files != null) {
			String name = managedFile + '.';
			int len = name.length();
			for (int i = 0; i < files.lengthi++) {
				if (!files[i].startsWith(name))
					continue;
				try {
					int generation = Integer.parseInt(files[i].substring(len));
					if (generation > oldestGeneration)
						oldestGeneration = generation;
catch (NumberFormatException e) {
					continue;
				}
			}
		}
		return oldestGeneration;
	}

Update the given managed files with the content in the given source files. The managedFiles is a list of managed file names which are currently managed. If a managed file name is not currently managed it will be added as a managed file for this storage manager. The sources are absolute (or relative to the current working directory) file paths containing the new content for the corresponding managed files.

Parameters:
managedFiles the managed files to update
sources the new content for the managed files
Throws:
java.io.IOException if there are any problems updating the given managed files
	public void update(String[] managedFilesString[] sourcesthrows IOException {
		if (!)
		if (!lock(true))
		try {
			int[] originalReadIDs = new int[managedFiles.length];
			boolean error = false;
			for (int i = 0; i < managedFiles.lengthi++) {
				originalReadIDs[i] = getId(managedFiles[i]);
				if (!update(managedFiles[i], sources[i]))
					error = true;
			}
			if (error) {
				// restore the original readIDs to avoid inconsistency for this group
				for (int i = 0; i < managedFiles.lengthi++) {
					Entry entry = (Entry.get(managedFiles[i]);
					entry.setReadId(originalReadIDs[i]);
				}
			}
			save(); //save only if no errors
finally {
		}
	}

Returns a list of all the managed files currently being managed.

Returns:
the names of the managed files
	public String[] getManagedFiles() {
		if (!)
			return null;
		Set set = .keySet();
		String[] keys = (String[]) set.toArray(new String[set.size()]);
		String[] result = new String[keys.length];
		for (int i = 0; i < keys.lengthi++)
			result[i] = new String(keys[i]);
		return result;
	}

Returns the directory containing the files being managed by this storage manager.

Returns:
the directory containing the managed files
	public File getBase() {
		return ;
	}

Returns the current numeric id (appendage) of the given managed file. managedFile + "." + getId(target). A value of -1 is returned if the given name is not managed.

Parameters:
managedFile the name of the managed file
Returns:
the id of the managed file
	public int getId(String managedFile) {
		if (!)
			return -1;
		Entry entry = (Entry.get(managedFile);
		if (entry == null)
			return -1;
		return entry.getReadId();
	}

Returns if readOnly state this storage manager is using.

Returns:
if this storage manager update state is read-only.
	public boolean isReadOnly() {
		return ;
	}
	/* (non-Javadoc)
	 * Attempts to lock the state of this manager and returns <code>true</code>
	 * if the lock could be acquired.
	 * <p>
	 * Locking a manager is advisory only. That is, it does not prevent other
	 * applications from modifying the files managed by this manager.
	 * </p>
	 * 
	 * @exception IOException
	 *                         if there was an unexpected problem while acquiring the
	 *                         lock.
	 */
	private boolean lock(boolean waitthrows IOException {
			return false;
		if ( == null) {
			 = BasicLocation.createLocker();
			if ( == null)
		}
		boolean locked = .lock();
		if (locked || !wait)
			return locked;
		//Someone else must have the directory locked, but they should release it quickly
		long start = System.currentTimeMillis();
		while (true) {
			try {
				Thread.sleep(200); // 5x per second
catch (InterruptedException e) {/*ignore*/
			}
			locked = .lock();
			if (locked)
				return true;
			// never wait longer than 5 seconds
			long time = System.currentTimeMillis() - start;
			if (time > )
				return false;
		}
	}

Returns the actual file location to use when reading the given managed file. A value of null can be returned if the given managed file name is not managed and add is set to false.

The returned file should be considered read-only. Any updates to the content of this file should be done using update(java.lang.String[],java.lang.String[]).

Parameters:
managedFile the managed file to lookup
add indicate whether the managed file name should be added to the manager if it is not already managed.
Returns:
the absolute file location to use for the given managed file or null if the given managed file is not managed
Throws:
java.io.IOException if the add flag is set to true and the addition of the managed file failed
	public File lookup(String managedFileboolean addthrows IOException {
		if (!)
		Entry entry = (Entry.get(managedFile);
		if (entry == null) {
			if (add) {
				add(managedFile);
				entry = (Entry.get(managedFile);
else {
				return null;
			}
		}
		return new File(getAbsolutePath(managedFile + '.' + entry.getReadId()));
	}
	private boolean move(String sourceString managedFile) {
		File original = new File(source);
		File targetFile = new File(managedFile);
		// its ok if the original does not exist. The table entry will capture
		// that fact. There is no need to put something in the filesystem.
		if (!original.exists() || targetFile.exists())
			return false;
		return original.renameTo(targetFile);
	}

Saves the state of the storage manager and releases any locks held.
	private void release() {
		if ( == null)
			return;
	}

Removes the given managed file from management by this storage manager.

Parameters:
managedFile the managed file to remove
Throws:
java.io.IOException if an error occured removing the managed file
	public void remove(String managedFilethrows IOException {
		if (!)
		// The removal needs to be done eagerly, so the value is effectively removed from the disktable. 
		// Otherwise, an updateTable() caused by an update(,)  could cause the file to readded to the local table.
		if (!lock(true))
		try {
			.remove(managedFile);
finally {
		}
	}
	private void updateTable() throws IOException {
		int stamp;
		stamp = ReliableFile.lastModifiedVersion();
		if (stamp ==  || stamp == -1)
			return;
		Properties diskTable = new Properties();
		try {
			diskTable.load(input);
finally {
			try {
				input.close();
catch (IOException e) {
				// ignore
			}
		}
		 = stamp;
		for (Enumeration e = diskTable.keys(); e.hasMoreElements();) {
			String file = (Stringe.nextElement();
			String value = diskTable.getProperty(file);
			if (value != null) {
				Entry entry = (Entry.get(file);
				// check front of value for ReliableFile
				int id;
				int fileType;
				int idx = value.indexOf(',');
				if (idx != -1) {
					id = Integer.parseInt(value.substring(0, idx));
					fileType = Integer.parseInt(value.substring(idx + 1));
else {
					id = Integer.parseInt(value);
					fileType = ;
				}
				if (entry == null) {
					.put(filenew Entry(idid + 1, fileType));
else {
					entry.setWriteId(id + 1);
					//don't change type
				}
			}
		}
	}
	/*
	 * This method should be called while the manager is locked.
	 */
	private void save() throws IOException {
			return;
		// if the table file has change on disk, update our data structures then
		// rewrite the file.
		Properties props = new Properties();
		for (Enumeration e = .keys(); e.hasMoreElements();) {
			String file = (Stringe.nextElement();
			Entry entry = (Entry.get(file);
			String value;
			if (entry.getFileType() != ) {
				value = Integer.toString(entry.getWriteId() - 1) + ',' + //In the table we save the write  number  - 1, because the read number can be totally different.
						Integer.toString(entry.getFileType());
else {
				value = Integer.toString(entry.getWriteId() - 1); //In the table we save the write  number  - 1, because the read number can be totally different.
			}
			props.put(filevalue);
		}
		boolean error = true;
		try {
			props.store(fileStream"safe table"); //$NON-NLS-1$
			fileStream.close();
			error = false;
finally {
			if (error)
				fileStream.abort();
		}
	}
	private boolean update(String managedFileString sourcethrows IOException {
		Entry entry = (Entry.get(managedFile);
		if (entry == null)
			add(managedFile);
		int newId = entry.getWriteId();
		// attempt to rename the file to the next generation
		boolean success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
		if (!success) {
			//possible the next write generation file exists? Lets determine the largest
			//generation number, then use that + 1.
			newId = findOldestGeneration(managedFile) + 1;
			success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
		}
		if (!success)
			return false;
		// update the entry. read and write ids should be the same since
		// everything is in sync
		entry.setReadId(newId);
		entry.setWriteId(newId + 1);
		return true;
	}

This methods remove all the temporary files that have been created by the storage manager. This removal is only done if the instance of eclipse calling this method is the last instance using this storage manager.

	private void cleanup() throws IOException {
			return;
		//Lock first, so someone else can not start while we're in the middle of cleanup
		if (!lock(true))
		try {
			//Iterate through the temp files and delete them all, except the one representing this storage manager.
			String[] files = .list();
			if (files != null) {
				for (int i = 0; i < files.lengthi++) {
					if (files[i].endsWith(".instance") &&  != null && !files[i].equalsIgnoreCase(.getName())) { //$NON-NLS-1$
						Locker tmpLocker = BasicLocation.createLocker(new File(files[i]), );
						if (tmpLocker.lock()) {
							//If I can lock it is a file that has been left behind by a crash
							tmpLocker.release();
							new File(files[i]).delete();
else {
							tmpLocker.release();
							return//The file is still being locked by somebody else
						}
					}
				}
			}
			//If we are here it is because we are the last instance running. After locking the table and getting its latest content, remove all the backup files and change the table
			Collection managedFiles = .entrySet();
			for (Iterator iter = managedFiles.iterator(); iter.hasNext();) {
				Map.Entry fileEntry = (Map.Entryiter.next();
				String fileName = (StringfileEntry.getKey();
				Entry info = (EntryfileEntry.getValue();
					ReliableFile.cleanupGenerations(new File(fileName));
else {
					//Because we are cleaning up, we are giving up the values from our table, and we must delete all the files that are not referenced by the table
					String readId = Integer.toString(info.getWriteId() - 1);
					deleteCopies(fileNamereadId);
				}
			}
			if () {
				files = .list();
				if (files != null) {
					for (int i = 0; i < files.lengthi++) {
						if (files[i].endsWith(.)) {
							new File(files[i]).delete();
						}
					}
				}
			}
finally {
		}
	}
	private void deleteCopies(String fileNameString exceptionNumber) {
		String notToDelete = fileName + '.' + exceptionNumber;
		String[] files = .list();
		if (files == null)
			return;
		for (int i = 0; i < files.lengthi++) {
			if (files[i].startsWith(fileName + '.') && !files[i].equals(notToDelete))
				new File(files[i]).delete();
		}
	}

This method declares the storage manager as closed. From thereon, the instance can no longer be used. It is important to close the manager as it also cleans up old copies of the managed files.
	public void close() {
		if (!)
			return;
		 = false;
			return;
		try {
catch (IOException e) {
			//Ignore and close.
		}
		if ( != null)
		if ( != null)
	}

This methods opens the storage manager. This method must be called before any operation on the storage manager.

Parameters:
wait indicates if the open operation must wait in case of contention on the lock file.
Throws:
java.io.IOException if an error occurred opening the storage manager
	public void open(boolean waitthrows IOException {
		if (!) {
			boolean locked = lock(wait);
			if (!locked && wait)
		}
		try {
			 = true;
finally {
		}
	}

Creates a new unique empty temporary-file in the storage manager base directory. The file name must be at least 3 characters. This file can later be used to update a managed file.

Parameters:
file the file name to create temporary file from.
Returns:
the newly-created empty file.
Throws:
java.io.IOException if the file can not be created.
See also:
update(java.lang.String[],java.lang.String[])
	public File createTempFile(String filethrows IOException {
		File tmpFile = File.createTempFile(file.);
		tmpFile.deleteOnExit();
		return tmpFile;
	}

Returns a managed InputStream for a managed file. null can be returned if the given name is not managed.

Parameters:
managedFile the name of the managed file to open.
Returns:
an input stream to the managed file or null if the given name is not managed.
Throws:
java.io.IOException if the content is missing, corrupt or an error occurs.
	public InputStream getInputStream(String managedFilethrows IOException {
	}

Returns a managed input stream set for the managed file names. Elements of the returned set may be null if a given name is not managed. This method should be used for managed file sets which use the output streams returned by the getOutputStreamSet(java.lang.String[]) to save data.

Parameters:
managedFiles the names of the managed files to open.
Returns:
a set input streams to the given managed files.
Throws:
java.io.IOException if the content of one of the managed files is missing, corrupt or an error occurs.
	public InputStream[] getInputStreamSet(String[] managedFilesthrows IOException {
		InputStream[] streams = new InputStream[managedFiles.length];
		for (int i = 0; i < streams.lengthi++)
			streams[i] = getInputStream(managedFiles[i], .);
		return streams;
	}
	private InputStream getInputStream(String managedFilesint openMaskthrows IOException {
			int id = getId(managedFiles);
			if (id == -1)
				return null;
			return new ReliableFileInputStream(new File(getBase(), managedFiles), idopenMask);
		}
		File lookup = lookup(managedFilesfalse);
		if (lookup == null)
			return null;
		return new FileInputStream(lookup);
	}

Returns a ManagedOutputStream for a managed file. Closing the ouput stream will update the storage manager with the new content of the managed file.

Parameters:
managedFile the name of the managed file to write.
Returns:
a managed output stream for the managed file.
Throws:
java.io.IOException if an error occurs opening the managed file.
	public ManagedOutputStream getOutputStream(String managedFilethrows IOException {
			ReliableFileOutputStream out = new ReliableFileOutputStream(new File(getBase(), managedFile));
			return new ManagedOutputStream(outthismanagedFilenull);
		}
		File tmpFile = createTempFile(managedFile);
		return new ManagedOutputStream(new FileOutputStream(tmpFile), thismanagedFiletmpFile);
	}

Returns an array of ManagedOutputStream for a set of managed files. When all managed output streams in the set have been closed, the storage manager will be updated with the new content of the managed files. Aborting any one of the streams will cause the entire content of the set to abort and be discarded.

Parameters:
managedFiles list of names of the managed file to write.
Returns:
an array of managed output streams respectively of managed files.
Throws:
java.io.IOException if an error occurs opening the managed files.
	public ManagedOutputStream[] getOutputStreamSet(String[] managedFilesthrows IOException {
		int count = managedFiles.length;
		ManagedOutputStream[] streams = new ManagedOutputStream[count];
		int idx = 0;
		try {
			for (; idx < countidx++) {
				ManagedOutputStream newStream = getOutputStream(managedFiles[idx]);
				newStream.setStreamSet(streams);
				streams[idx] = newStream;
			}
catch (IOException e) {
			// cleanup
			for (int jdx = 0; jdx < idxjdx++)
				streams[jdx].abort();
			throw e;
		}
		return streams;
	}
	/* (non-Javadoc)
	 * Instructs this manager to abort and discard a managed output stream.
	 * This method should be used if any errors occur after opening a managed
	 * output stream where the contents should not be saved.
	 * If this output stream is part of a set, all other managed output streams in this set
	 * will also be closed and aborted.
	 * @param out the managed output stream
	 * @see #getOutputStream(String)
	 * @see #getOutputStreamSet(String[])
	 */
		if (set == null) {
			set = new ManagedOutputStream[] {out};
		}
		synchronized (set) {
			for (int idx = 0; idx < set.lengthidx++) {
				out = set[idx];
				if (out.getOutputFile() == null) {
					// this is a ReliableFileOutpuStream
					rfos.abort();
else {
					// plain FileOutputStream();
						try {
catch (IOException e) {/*do nothing*/
						}
					}
				}
			}
		}
	}
	/* (non-Javadoc)
	 * Close the managed output stream and update the new content to  
	 * this manager. If this managed output stream is part of a set, only after closing
	 * all managed output streams in the set will storage manager be updated.
	 * 
	 * @param smos the output stream.
	 * @throws IOException if an errors occur.
	 * @see #getOutputStream(String)
	 * @see #getOutputStreamSet(String[])
	 */
			return;
		ManagedOutputStream[] streamSet = smos.getStreamSet();
		if (smos.getOutputFile() == null) {
			// this is a ReliableFileOutputStream
			// manage file deletes
			File file = rfos.closeIntermediateFile();
			String target = smos.getTarget();
			if (streamSet == null) {
				update(new String[] {smos.getTarget()}, new String[] {file.getName()});
				ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
			}
else {
			// this is a plain old file output steam
			out.flush();
			try {
catch (SyncFailedException e) {/*ignore*/
			}
			out.close();
			String target = smos.getTarget();
			if (streamSet == null) {
				update(new String[] {target}, new String[] {smos.getOutputFile().getName()});
			}
		}
		if (streamSet != null) {
			synchronized (streamSet) {
				//check all the streams to see if there are any left open....
				for (int idx = 0; idx < streamSet.lengthidx++) {
					if (streamSet[idx].getState() == .)
						return//done
				}
				//all streams are closed, we need to update storage manager
				String[] targets = new String[streamSet.length];
				String[] sources = new String[streamSet.length];
				for (int idx = 0; idx < streamSet.lengthidx++) {
					smos = streamSet[idx];
					targets[idx] = smos.getTarget();
					File outputFile = smos.getOutputFile();
					if (outputFile == null) {
						// this is a ReliableFile 
						File file = rfos.closeIntermediateFile(); //multiple calls to close() ok
						sources[idx] = file.getName();
						ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
else {
						sources[idx] = outputFile.getName();
					}
				}
				update(targetssources);
			}
		}
	}
New to GrepCode? Check out our FAQ X