EntityTagVirtualAttributeProvider.java revision ea1068c292e9b341af6d6b563cd8988a96be20a9
/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.txt.
* 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
*
*
* Portions Copyright 2012-2015 ForgeRock AS.
*/
package org.opends.server.extensions;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.zip.Adler32;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.EntityTagVirtualAttributeCfg;
import org.opends.server.api.VirtualAttributeProvider;
import org.opends.server.core.SearchOperation;
import org.opends.server.types.*;
import org.opends.server.util.StaticUtils;
import static org.opends.messages.ExtensionMessages.*;
/**
* This class implements a virtual attribute provider which ensures that all
* entries contain an "entity tag" or "Etag" as defined in section 3.11 of RFC
* 2616.
* <p>
* The entity tag may be used by clients, in conjunction with the assertion
* control, for optimistic concurrency control, as a way to help prevent
* simultaneous updates of an entry from conflicting with each other.
*/
public final class EntityTagVirtualAttributeProvider extends
VirtualAttributeProvider<EntityTagVirtualAttributeCfg> implements
ConfigurationChangeListener<EntityTagVirtualAttributeCfg>
{
private static final Comparator<Attribute> ATTRIBUTE_COMPARATOR =
new Comparator<Attribute>()
{
/**
* {@inheritDoc}
*/
@Override
public int compare(final Attribute a1, final Attribute a2)
{
return a1.getNameWithOptions().compareTo(a2.getNameWithOptions());
}
};
// Current configuration.
private volatile EntityTagVirtualAttributeCfg config;
/**
* Default constructor invoked by reflection.
*/
public EntityTagVirtualAttributeProvider()
{
// Initialization performed by initializeVirtualAttributeProvider.
}
/**
* {@inheritDoc}
*/
@Override
public ConfigChangeResult applyConfigurationChange(
final EntityTagVirtualAttributeCfg configuration)
{
this.config = configuration;
return new ConfigChangeResult();
}
/**
* {@inheritDoc}
*/
@Override
public ConditionResult approximatelyEqualTo(final Entry entry,
final VirtualAttributeRule rule, final ByteString value)
{
// ETags cannot be used in approximate matching.
return ConditionResult.UNDEFINED;
}
/**
* {@inheritDoc}
*/
@Override
public void finalizeVirtualAttributeProvider()
{
config.removeEntityTagChangeListener(this);
}
/**
* {@inheritDoc}
*/
@Override
public Attribute getValues(final Entry entry, final VirtualAttributeRule rule)
{
// Save reference to current configuration in case it changes.
final EntityTagVirtualAttributeCfg cfg = config;
// Determine which checksum algorithm to use.
final Checksum checksummer;
switch (cfg.getChecksumAlgorithm())
{
case CRC_32:
checksummer = new CRC32();
break;
default: // ADLER_32
checksummer = new Adler32();
break;
}
final ByteString etag = checksumEntry(cfg, checksummer, entry);
return Attributes.create(rule.getAttributeType(), etag);
}
/**
* {@inheritDoc}
*/
@Override
public ConditionResult greaterThanOrEqualTo(final Entry entry,
final VirtualAttributeRule rule, final ByteString value)
{
// ETags cannot be used in ordering matching.
return ConditionResult.UNDEFINED;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasValue(final Entry entry, final VirtualAttributeRule rule)
{
// ETag is always present.
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void initializeVirtualAttributeProvider(
final EntityTagVirtualAttributeCfg configuration) throws ConfigException,
InitializationException
{
this.config = configuration;
configuration.addEntityTagChangeListener(this);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isConfigurationChangeAcceptable(
final EntityTagVirtualAttributeCfg configuration,
final List<LocalizableMessage> unacceptableReasons)
{
// The new configuration should always be acceptable.
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMultiValued()
{
// ETag is always single-valued.
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSearchable(final VirtualAttributeRule rule,
final SearchOperation searchOperation,
final boolean isPreIndexed)
{
// ETags cannot be searched since there is no way to determine which entry
// is associated with a particular ETag.
return false;
}
/**
* {@inheritDoc}
*/
@Override
public ConditionResult lessThanOrEqualTo(final Entry entry,
final VirtualAttributeRule rule, final ByteString value)
{
// ETags cannot be used in ordering matching.
return ConditionResult.UNDEFINED;
}
/**
* {@inheritDoc}
*/
@Override
public ConditionResult matchesSubstring(final Entry entry,
final VirtualAttributeRule rule, final ByteString subInitial,
final List<ByteString> subAny, final ByteString subFinal)
{
// ETags cannot be used in substring matching.
return ConditionResult.UNDEFINED;
}
/**
* {@inheritDoc}
*/
@Override
public void processSearch(final VirtualAttributeRule rule,
final SearchOperation searchOperation)
{
final LocalizableMessage message = ERR_ETAG_VATTR_NOT_SEARCHABLE.get(rule
.getAttributeType().getNameOrOID());
searchOperation.appendErrorMessage(message);
searchOperation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
}
private void checksumAttribute(final EntityTagVirtualAttributeCfg cfg,
final Checksum checksummer, final Attribute attribute)
{
// Object class may be null.
if (attribute == null)
{
return;
}
// Ignore other virtual attributes include this one.
if (attribute.isVirtual())
{
return;
}
// Ignore excluded attributes.
if (cfg.getExcludedAttribute().contains(attribute.getAttributeType()))
{
return;
}
// Checksum the attribute description.
final String atd = attribute.getNameWithOptions();
final byte[] bytes = StaticUtils.getBytes(atd);
checksummer.update(bytes, 0, bytes.length);
// Checksum the attribute values. The value order may vary between
// replicas so we need to make sure that we always process them in the
// same order. Note that we don't need to normalize the values since we want
// to detect any kind of updates even if they are not semantically
// significant. In any case, normalization can be expensive and should be
// avoided if possible.
final int size = attribute.size();
switch (size)
{
case 0:
// It's surprising to have an empty attribute, but if we do then there's
// nothing to do.
break;
case 1:
// Avoid sorting single valued attributes.
checksumValue(checksummer, attribute.iterator().next());
break;
default:
// Multi-valued attributes need sorting.
final ByteString[] values = new ByteString[size];
int i = 0;
for (final ByteString av : attribute)
{
values[i++] = av;
}
Arrays.sort(values);
for (final ByteString value : values)
{
checksumValue(checksummer, value);
}
break;
}
}
private ByteString checksumEntry(final EntityTagVirtualAttributeCfg cfg,
final Checksum checksummer, final Entry entry)
{
// Checksum the object classes since these are not included in the entry's
// attributes.
checksumAttribute(cfg, checksummer, entry.getObjectClassAttribute());
// The attribute order may vary between replicas so we need to make sure
// that we always process them in the same order.
final List<Attribute> attributes = entry.getAttributes();
Collections.sort(attributes, ATTRIBUTE_COMPARATOR);
for (final Attribute attribute : attributes)
{
checksumAttribute(cfg, checksummer, attribute);
}
// Convert the checksum value to a hex string.
long checksum = checksummer.getValue();
final byte[] bytes = new byte[16];
int j = 15;
for (int i = 7; i >= 0; i--)
{
final byte b = (byte) (checksum & 0xFF);
final byte l = (byte) (b & 0x0F);
bytes[j--] = (byte) (l < 10 ? l + 48 : l + 87);
final byte h = (byte) ((b & 0xF0) >>> 4);
bytes[j--] = (byte) (h < 10 ? h + 48 : h + 87);
checksum >>>= 8;
}
return ByteString.wrap(bytes);
}
private void checksumValue(final Checksum checksummer, final ByteString value)
{
final int size = value.length();
for (int i = 0; i < size; i++)
{
checksummer.update(value.byteAt(i) & 0xFF);
}
}
}