/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2013 ForgeRock AS
*/
package org.opends.server.tools.upgrade;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.messages.ToolMessages.*;
import static org.opends.server.tools.upgrade.FileManager.*;
import static org.opends.server.tools.upgrade.Installation.*;
import static org.opends.server.util.ServerConstants.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entries;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LinkedHashMapEntry;
import org.forgerock.opendj.ldap.Matcher;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.SchemaBuilder;
import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.ldif.LDIFEntryWriter;
import org.opends.server.controls.PersistentSearchChangeType;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.util.SetupUtils;
import org.opends.server.util.StaticUtils;
/**
* Common utility methods needed by the upgrade.
*/
final class UpgradeUtils
{
/**
* Upgrade's logger.
*/
private final static Logger LOG = Logger
.getLogger(UpgradeCli.class.getName());
/** The config folder of the current installation. */
static final File configDirectory = new File(getInstallationPath(),
Installation.CONFIG_PATH_RELATIVE);
/** The config/schema folder of the current installation. */
static final File configSchemaDirectory = new File(getInstallationPath(),
Installation.CONFIG_PATH_RELATIVE + File.separator
+ Installation.SCHEMA_PATH_RELATIVE);
/** The template folder of the current installation. */
static final File templateDirectory = new File(getInstallationPath(),
Installation.CONFIG_PATH_RELATIVE + File.separator
+ Installation.TEMPLATE_RELATIVE_PATH);
/** The template/config/schema folder of the current installation. */
static final File templateConfigSchemaDirectory = new File(
getInstallationPath(), Installation.TEMPLATE_RELATIVE_PATH
+ File.separator + Installation.CONFIG_PATH_RELATIVE
+ File.separator + Installation.SCHEMA_PATH_RELATIVE);
/** The template/config folder of the current installation. */
static final File templateConfigDirectory = new File(
getInstallationPath(), Installation.TEMPLATE_RELATIVE_PATH
+ File.separator + Installation.CONFIG_PATH_RELATIVE);
/** The config snmp security folder of the current installation. */
static final File configSnmpSecurityDirectory = new File(
getInstallationPath(), Installation.CONFIG_PATH_RELATIVE
+ File.separator + Installation.SNMP_PATH_RELATIVE + File.separator
+ Installation.SECURITY_PATH_RELATIVE);
/**
* Returns the path of the installation of the directory server. Note that
* this method assumes that this code is being run locally.
*
* @return the path of the installation of the directory server.
*/
public static String getInstallPathFromClasspath()
{
String installPath = DirectoryServer.getServerRoot();
if (installPath != null)
{
return installPath;
}
/* Get the install path from the Class Path */
final String sep = System.getProperty("path.separator");
final String[] classPaths =
System.getProperty("java.class.path").split(sep);
String path = getInstallPath(classPaths);
if (path != null)
{
final File f = new File(path).getAbsoluteFile();
final File librariesDir = f.getParentFile();
/*
* Do a best effort to avoid having a relative representation (for
* instance to avoid having ../../../).
*/
try
{
installPath = librariesDir.getParentFile().getCanonicalPath();
}
catch (IOException ioe)
{
// Best effort
installPath = librariesDir.getParent();
}
}
return installPath;
}
private static String getInstallPath(final String[] classPaths)
{
for (String classPath : classPaths)
{
final String normPath = classPath.replace(File.separatorChar, '/');
if (normPath.endsWith(Installation.OPENDJ_BOOTSTRAP_JAR_RELATIVE_PATH))
{
return classPath;
}
}
return null;
}
/**
* Returns the path of the installation of the directory server. Note that
* this method assumes that this code is being run locally.
*
* @param installPath
* The installation path
* @return the path of the installation of the directory server.
*/
public static String getInstancePathFromInstallPath(final String installPath)
{
String instancePathFileName = Installation.INSTANCE_LOCATION_PATH;
final File configureScriptPath =
new File(installPath + File.separator
+ Installation.UNIX_CONFIGURE_FILE_NAME);
// look for /etc/opt/opends/instance.loc
File f = new File(instancePathFileName);
if (!configureScriptPath.exists() || !f.exists())
{
// look for <installPath>/instance.loc
instancePathFileName =
installPath + File.separator
+ Installation.INSTANCE_LOCATION_PATH_RELATIVE;
f = new File(instancePathFileName);
if (!f.exists())
{
return installPath;
}
}
BufferedReader reader;
try
{
reader = new BufferedReader(new FileReader(instancePathFileName));
}
catch (Exception e)
{
return installPath;
}
// Read the first line and close the file.
String line;
try
{
line = reader.readLine();
File instanceLoc = new File(line.trim());
if (instanceLoc.isAbsolute())
{
return instanceLoc.getAbsolutePath();
}
else
{
return new File(installPath + File.separator + instanceLoc.getPath())
.getAbsolutePath();
}
}
catch (Exception e)
{
return installPath;
}
finally
{
StaticUtils.close(reader);
}
}
/**
* Returns the absolute path for the given file. It tries to get the canonical
* file path. If it fails it returns the string representation.
*
* @param f
* File to get the path
* @return the absolute path for the given file.
*/
public static String getPath(File f)
{
String path = null;
if (f != null)
{
try
{
/*
* Do a best effort to avoid having a relative representation (for
* instance to avoid having ../../../).
*/
File canonical = f.getCanonicalFile();
f = canonical;
}
catch (IOException ioe)
{
/*
* This is a best effort to get the best possible representation of the
* file: reporting the error is not necessary.
*/
}
path = f.toString();
}
return path;
}
/**
* Returns the absolute path for the given parentPath and relativePath.
*
* @param parentPath
* the parent path.
* @param relativePath
* the relative path.
* @return the absolute path for the given parentPath and relativePath.
*/
public static String getPath(final String parentPath,
final String relativePath)
{
return getPath(new File(new File(parentPath), relativePath));
}
/**
* Returns <CODE>true</CODE> if we are running under windows and
* <CODE>false</CODE> otherwise.
*
* @return <CODE>true</CODE> if we are running under windows and
* <CODE>false</CODE> otherwise.
*/
public static boolean isWindows()
{
return SetupUtils.isWindows();
}
/**
* Returns <CODE>true</CODE> if we are running under Unix and
* <CODE>false</CODE> otherwise.
*
* @return <CODE>true</CODE> if we are running under Unix and
* <CODE>false</CODE> otherwise.
*/
public static boolean isUnix()
{
return SetupUtils.isUnix();
}
/**
* Determines whether one file is the parent of another.
*
* @param ancestor
* possible parent of <code>descendant</code>
* @param descendant
* possible child 0f <code>ancestor</code>
* @return return true if ancestor is a parent of descendant
*/
static public boolean isParentOf(final File ancestor, File descendant)
{
if (ancestor != null)
{
if (ancestor.equals(descendant))
{
return false;
}
while ((descendant != null) && !ancestor.equals(descendant))
{
descendant = descendant.getParentFile();
}
}
return (ancestor != null) && (descendant != null);
}
/**
* Returns <CODE>true</CODE> if the first provided path is under the second
* path in the file system.
* @param descendant the descendant candidate path.
* @param path the path.
* @return <CODE>true</CODE> if the first provided path is under the second
* path in the file system; <code>false</code> otherwise or if
* either of the files are null
*/
public static boolean isDescendant(File descendant, File path) {
boolean isDescendant = false;
if (descendant != null && path != null) {
File parent = descendant.getParentFile();
while ((parent != null) && !isDescendant) {
isDescendant = path.equals(parent);
if (!isDescendant) {
parent = parent.getParentFile();
}
}
}
return isDescendant;
}
/**
* Returns the instance root directory (the path where the instance is
* installed).
*
* @return the instance root directory (the path where the instance is
* installed).
*/
static final String getInstancePath()
{
final String installPath = getInstallationPath();
if (installPath == null)
{
return null;
}
return getInstancePathFromInstallPath(installPath);
}
/**
* Returns the server's installation path.
*
* @return The server's installation path.
*/
static final String getInstallationPath()
{
// The upgrade runs from the bits extracted by BuildExtractor
// in the staging directory. However
// we still want the Installation to point at the build being
// upgraded so the install path reported in [installroot].
String installationPath = System.getProperty("INSTALL_ROOT");
if (installationPath == null)
{
final String path = getInstallPathFromClasspath();
if (path != null)
{
final File f = new File(path);
if (f.getParentFile() != null
&& f.getParentFile().getParentFile() != null
&& new File(f.getParentFile().getParentFile(),
Installation.LOCKS_PATH_RELATIVE).exists())
{
installationPath = getPath(f.getParentFile().getParentFile());
}
else
{
installationPath = path;
}
}
}
return installationPath;
}
// This function is not in use actually but may be useful later
// eg. for rebuild index task.
@SuppressWarnings("unused")
private static final SortedMap<String, LinkedList<String>> getLocalBackends()
{
// Config.ldif path
final File configLdif = new File(configDirectory,
CURRENT_CONFIG_FILE_NAME);
SortedMap<String, LinkedList<String>> result =
new TreeMap<String, LinkedList<String>>();
LDIFEntryReader entryReader = null;
try
{
entryReader = new LDIFEntryReader(new FileInputStream(configLdif));
final Filter filter =
Filter.equality("objectclass", "ds-cfg-local-db-backend");
final Matcher includeFilter = filter.matcher();
entryReader.setIncludeFilter(includeFilter);
Entry entry = null;
while (entryReader.hasNext())
{
LinkedList<String> dataRelativesToBck = new LinkedList<String>();
entry = entryReader.readEntry();
// db path
dataRelativesToBck.add(entry.getAttribute("ds-cfg-db-directory")
.firstValueAsString());
// enabled ?
dataRelativesToBck.add(entry.getAttribute("ds-cfg-enabled")
.firstValueAsString());
// backend name
result.put(
entry.getAttribute("ds-cfg-backend-id").firstValueAsString(),
dataRelativesToBck);
}
}
catch (Exception ex)
{
LOG.log(Level.SEVERE, ex.getMessage());
}
finally
{
StaticUtils.close(entryReader);
}
return result;
}
/**
* Updates the config file during the upgrade process.
*
* @param configPath
* The original path to the file.
* @param filter
* The filter to avoid files.
* @param changeType
* The change type which must be applied to ldif lines.
* @param lines
* The change record ldif lines.
* @throws IOException
* If an Exception occurs during the input output methods.
* @return The changes number that have occurred.
*/
static final int updateConfigFile(final String configPath,
final LDAPFilter filter, final PersistentSearchChangeType changeType,
final String... lines) throws IOException
{
final File original = new File(configPath);
final File copyConfig =
File.createTempFile("copyConfig", ".tmp", original.getParentFile());
int changeCount = 0;
LDIFEntryReader entryReader = null;
LDIFEntryWriter writer = null;
try
{
entryReader = new LDIFEntryReader(new FileInputStream(configPath));
entryReader.setSchemaValidationPolicy(SchemaValidationPolicy.ignoreAll());
writer = new LDIFEntryWriter(new FileOutputStream(copyConfig));
writer.setWrapColumn(80);
// Writes the header on the new file.
writer.writeComment(INFO_CONFIG_FILE_HEADER.get());
writer.setWrapColumn(0);
Entry entry = null;
boolean alreadyExist = false;
while (entryReader.hasNext())
{
entry = entryReader.readEntry();
// Searching for the related entries
if (filter != null
&& Filter.valueOf(filter.toString()).matches(entry)
== ConditionResult.TRUE)
{
try
{
final ModifyRequest mr =
Requests.newModifyRequest(readLDIFLines(entry.getName(),
changeType, lines));
entry = Entries.modifyEntryPermissive(entry, mr.getModifications());
changeCount++;
LOG.log(Level.INFO,
String.format("The following entry has been modified : %s",
entry.getName()));
}
catch (Exception ex)
{
LOG.log(Level.SEVERE, ex.getMessage());
}
}
if (filter == null && changeType == PersistentSearchChangeType.ADD
&& ("dn: " + entry.getName()).equals(lines[0]))
{
LOG.log(Level.INFO, String.format("Entry %s found", entry.getName()
.toString()));
alreadyExist = true;
}
writer.writeEntry(entry);
}
if (filter == null && changeType == PersistentSearchChangeType.ADD
&& !alreadyExist)
{
final AddRequest ar = Requests.newAddRequest(lines);
writer.writeEntry(ar);
LOG.log(Level.INFO, String.format("Entry successfully added %s in %s",
entry.getName().toString(), original.getAbsolutePath()));
changeCount++;
}
}
catch (Exception ex)
{
throw new IOException(ex.getMessage());
}
finally
{
// The reader and writer must be close before writing files.
// This causes exceptions under windozs OS.
StaticUtils.close(entryReader, writer);
}
try
{
// Renaming the file, overwriting previous one.
rename(copyConfig, new File(configPath));
}
catch (IOException e)
{
LOG.log(Level.SEVERE, e.getMessage());
deleteRecursively(original);
throw e;
}
return changeCount;
}
/**
* This task adds new attributes / object classes to the specified destination
* file. The new attributes and object classes must be originaly defined in
* the template file.
*
* @param templateFile
* The file in which the new attribute/object definition can be read.
* @param destination
* The file where we want to add the new definitions.
* @param attributes
* Those attributes needed to be inserted into the new destination
* file.
* @param objectClasses
* Those object classes needed to be inserted into the new
* destination file.
* @return An integer which represents each time an attribute / object class
* is inserted successfully to the destination file.
* @throws IOException
* If an unexpected IO error occurred while reading the entry.
*/
static int updateSchemaFile(final File templateFile, final File destination,
final String[] attributes, final String[] objectClasses)
throws IOException
{
int changeCount = 0;
LDIFEntryReader reader = null;
BufferedReader br = null;
FileWriter fw = null;
final File copy =
File.createTempFile("copySchema", ".tmp",
destination.getParentFile());
try
{
reader = new LDIFEntryReader(new FileInputStream(templateFile));
final LinkedList<String> definitionsList = new LinkedList<String>();
final Entry schemaEntry = reader.readEntry();
Schema schema = null;
schema =
new SchemaBuilder(Schema.getCoreSchema())
.addSchema(schemaEntry, true).toSchema();
if (attributes != null)
{
for (final String att : attributes)
{
try
{
final String definition =
"attributeTypes: " + schema.getAttributeType(att);
definitionsList.add(definition);
LOG.log(Level.INFO, String.format("Added %s", definition));
}
catch (UnknownSchemaElementException e)
{
LOG.log(Level.SEVERE, ERR_UPGRADE_UNKNOWN_OC_ATT.get("attribute",
att).toString());
}
}
}
if (objectClasses != null)
{
for (final String oc : objectClasses)
{
try
{
final String definition =
"objectClasses: " + schema.getObjectClass(oc);
definitionsList.add(definition);
LOG.log(Level.INFO, String.format("Added %s", definition));
}
catch (UnknownSchemaElementException e)
{
LOG.log(Level.SEVERE, ERR_UPGRADE_UNKNOWN_OC_ATT.get(
"object class", oc).toString());
}
}
}
// Then, open the destination file and write the new attribute
// or objectClass definitions
br = new BufferedReader(new FileReader(destination));
fw = new FileWriter(copy);
String line = br.readLine();
while (line != null && !"".equals(line))
{
fw.write(line + EOL);
line = br.readLine();
}
for (final String definition : definitionsList)
{
writeLine(fw, definition, 80);
changeCount++;
}
// Must be ended with a blank line
fw.write(EOL);
}
finally
{
// The reader and writer must be close before writing files.
// This causes exceptions under windows OS.
StaticUtils.close(br, fw, reader);
}
// Writes the schema file.
try
{
rename(copy, destination);
}
catch (IOException e)
{
LOG.log(Level.SEVERE, e.getMessage());
deleteRecursively(copy);
throw e;
}
return changeCount;
}
/**
* Creates a new file in the config/upgrade folder. The new file is a
* concatenation of entries of all files contained in the config/schema
* folder.
*
* @param folder
* The folder containing the schema files.
* @param revision
* The revision number of the current binary version.
* @throws IOException
* If we cannot read the files contained in the folder where the
* schema files are supposed to be.
*/
static void updateConfigUpgradeSchemaFile(final File folder,
final String revision) throws IOException
{
// We need to upgrade the schema.ldif.<rev> file contained in the
// config/upgrade folder otherwise, we cannot enable the backend at
// server's start. We need to read all files contained in config/schema
// and add all attribute/object classes in this new super entry which
// will be read at start-up.
Entry theNewSchemaEntry = new LinkedHashMapEntry();
LDIFEntryReader reader = null;
LDIFEntryWriter writer = null;
try
{
if (folder.isDirectory())
{
for (final File f : folder.listFiles())
{
LOG.log(Level.INFO, String.format("Processing %s", f
.getAbsolutePath()));
reader = new LDIFEntryReader(new FileInputStream(f));
while (reader.hasNext())
{
try
{
final Entry entry = reader.readEntry();
theNewSchemaEntry.setName(entry.getName());
for (final Attribute at : entry.getAllAttributes())
{
theNewSchemaEntry.addAttribute(at);
}
}
catch (Exception ex)
{
LOG.log(Level.SEVERE, ex.getMessage());
}
}
}
// Creates a File object representing
// config/upgrade/schema.ldif.revision which the server creates
// the first time it starts if there are schema customizations.
final File destination =
new File(configDirectory, Installation.UPGRADE_PATH
+ File.separator + "schema.ldif." + revision);
writer = new LDIFEntryWriter(new FileOutputStream(destination));
writer.writeEntry(theNewSchemaEntry);
LOG.log(Level.INFO, String.format("%s file created successfully.",
destination.getAbsolutePath()));
}
}
finally
{
reader.close();
writer.close();
}
}
private static String[] readLDIFLines(final DN dn,
final PersistentSearchChangeType changeType, final String... lines)
{
final String[] modifiedLines = new String[lines.length + 2];
int index = 0;
if (changeType == PersistentSearchChangeType.MODIFY)
{
modifiedLines[0] = "dn: " + dn;
modifiedLines[1] = "changetype: modify";
index = 2;
}
for (final String line : lines)
{
modifiedLines[index] = line;
index++;
}
return modifiedLines;
}
private static void writeLine(final FileWriter fw, final String line,
final int wrapColumn) throws IOException
{
final int length = line.length();
if (length > wrapColumn)
{
fw.write(line.subSequence(0, wrapColumn).toString());
fw.write(EOL);
int pos = wrapColumn;
while (pos < length)
{
final int writeLength = Math.min(wrapColumn - 1, length - pos);
fw.write(" ");
fw.write(line.subSequence(pos, pos + writeLength).toString());
fw.write(EOL);
pos += wrapColumn - 1;
}
}
else
{
fw.write(line);
fw.write(EOL);
}
}
// Prevent instantiation.
private UpgradeUtils()
{
throw new AssertionError();
}
}