![JAR search and dependency download from the Maven repository](/logo.png)
com.unboundid.scim.ldap.ConstructedValue Maven / Gradle / Ivy
/*
* Copyright 2011-2019 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.scim.ldap;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.scim.sdk.Debug;
import com.unboundid.util.StaticUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class is used to construct DN and attribute values from a template value
* that allows portions of the DN and entry attributes to be included in the
* constructed attribute value / DN.
*
* This class is based on the UnboundID Sync Server implementation.
*/
public final class ConstructedValue
{
// The provided template is broken up into chunks. Each chunk is either
// fixed text; an attribute replacement, e.g. {uid}, that is replaced with
// the value from the source entry; or a DN component replacement that
// substitutes a piece of the DN, e.g. {1}.
private final List chunks;
// These are the DN replacement components that appear in the value template.
// For example, [1, 3] for a template that includes {1} and {3}.
private final SortedSet dnIndexReplacements;
// These are the source attributes that appear in the value template.
// For example, ["givenname", "sn"] for the cn={givnenname} {sn} template.
private final SortedSet entryAttrReplacements;
/**
* Constructs a value out of the specified template.
*
* @param template The template for the constructed value.
*/
public ConstructedValue(final String template)
{
List chunkList = new ArrayList();
TreeSet entryAttrs = new TreeSet(
String.CASE_INSENSITIVE_ORDER);
TreeSet dnComponentIndexes = new TreeSet();
int pos = 0;
while (pos < template.length())
{
int nextReplacementStart = template.indexOf('{', pos);
int fixedTextEnd;
if (nextReplacementStart == -1)
{
int nextReplacementEnd = template.indexOf('}', pos);
if (nextReplacementEnd != -1)
{
throw new IllegalArgumentException(
String.format(
"'%s' has mismatched or nested {} around a replacement value",
template));
}
fixedTextEnd = template.length();
}
else
{
fixedTextEnd = nextReplacementStart;
}
if (fixedTextEnd > pos)
{
chunkList.add(new FixedTextChunk(
template.substring(pos, fixedTextEnd)));
pos = fixedTextEnd;
}
if (nextReplacementStart != -1)
{
int nextReplacementEnd = template.indexOf('}', nextReplacementStart);
if (nextReplacementEnd == -1)
{
throw new IllegalArgumentException(
String.format(
"'%s' has mismatched or nested {} around a replacement value",
template));
}
String replacementStr =
template.substring(nextReplacementStart + 1, nextReplacementEnd);
AttributeValueChunk attrValueChunk;
if (replacementStr.contains(":"))
{
attrValueChunk = new RegExAttributeValueChunk(replacementStr);
}
else
{
attrValueChunk = new AttributeValueChunk(replacementStr);
}
chunkList.add(attrValueChunk);
if (attrValueChunk.isDnComponent())
{
dnComponentIndexes.add(attrValueChunk.getDnComponentIndex());
}
else
{
entryAttrs.add(attrValueChunk.getAttributeName());
}
pos = nextReplacementEnd + 1;
}
}
this.entryAttrReplacements =
Collections.unmodifiableSortedSet(entryAttrs);
this.dnIndexReplacements =
Collections.unmodifiableSortedSet(dnComponentIndexes);
this.chunks = Collections.unmodifiableList(chunkList);
}
/**
* Return the DN index replacements that appear in the value, e.g. [1, 3].
*
* @return The DN index replacements that appear in the value.
*/
SortedSet getDnIndexReplacements()
{
return dnIndexReplacements;
}
/**
* Return the source entry attribute replacements that appear in the value,
* e.g. ["givenname", "sn"].
*
* @return The source entry attribute replacements that appear in the value.
*/
SortedSet getEntryAttrReplacements()
{
return entryAttrReplacements;
}
/**
* Construct a value from the provided DN components and entry.
*
* @param dnComponents The DN components.
* @param entry The entry.
*
* @return The constructed value.
*/
String constructValue(final List dnComponents, final Entry entry)
{
StringBuilder value = new StringBuilder();
for (Chunk chunk : chunks)
{
//If there is a preceding chunk and it ended in an '=', then the current
//chunk must be an attribute value in a dn element. Otherwise, the current
//chunk represents one or more complete elements.
if (value.length() > 0 && value.charAt(value.length()-1) == '=')
{
value.append(chunk.getValue(dnComponents, entry, true));
}
else
{
value.append(chunk.getValue(dnComponents, entry, false));
}
}
return value.toString();
}
/**
* Construct a value from the provided entry.
*
* @param entry The entry.
*
* @return The constructed value.
*/
String constructValue(final Entry entry)
{
List noDnComponents = Collections.emptyList();
return constructValue(noDnComponents, entry);
}
/**
* Represents a single "chunk" of a constructed value which can either be
* fixed text, a replacement DN comonent, or a replacment attribute value. The
* final two options can use regular expression replacement values.
*/
private abstract static class Chunk
{
/**
* Return the value constructed out the specified components.
*
* @param dnComponents The DN components.
* @param entry The entry.
* @param isAttrValue True if the constructed value will be used
* as an attribute value of a dn element and
* should be escaped. False if the constructed
* value is itself one or more dn elements
* and does not need to be escaped
*
* @return The value constructed out the specified components.
*/
abstract String getValue(List dnComponents, Entry entry,
boolean isAttrValue);
}
/**
* A "chunk" in the replacement value that is fixed text.
*/
private static final class FixedTextChunk extends Chunk
{
private final String fixedText;
/**
* Constructor for the fixed test chunk.
*
* @param fixedText The fixed text.
*/
private FixedTextChunk(final String fixedText)
{
this.fixedText = fixedText;
}
/**
* {@inheritDoc}
*/
@Override
String getValue(final List dnComponents, final Entry entry,
final boolean isAttrValue)
{
return fixedText;
}
}
private static String ATTRIBUTE_ONLY_REGEX =
"[a-zA-Z0-9_\\.;\\-]+(?:,[a-zA-Z0-9_\\.;\\-]+)*";
private static Pattern ATTRIBUTE_REGEX =
Pattern.compile("^" + ATTRIBUTE_ONLY_REGEX + "$");
/**
* A "chunk" in the replacement value that is a value from the source entry,
* e.g. {givenname}.
*/
private static class AttributeValueChunk extends Chunk
{
private final boolean isDnComponent;
private final String attributeName;
private final int dnComponentIndex;
/**
* Constructor.
*
* @param attributeName The name of the attribute from the source entry.
*/
AttributeValueChunk(final String attributeName)
{
this.attributeName = attributeName;
Matcher matcher = ATTRIBUTE_REGEX.matcher(attributeName);
if (!matcher.matches())
{
throw new IllegalArgumentException(
String.format("'%s' is not a valid attribute name", attributeName));
}
if (attributeName.matches("\\d+"))
{
dnComponentIndex = Integer.parseInt(attributeName);
if (dnComponentIndex == 0)
{
throw new IllegalArgumentException(
String.format("'%s' is not a valid DN component replacement " +
"value. To use the value of the first matching " +
"component, use '{1}'", attributeName));
}
isDnComponent = true;
}
else
{
isDnComponent = false;
dnComponentIndex = 0;
}
}
/**
* {@inheritDoc}
*/
@Override
String getValue(final List dnComponents, final Entry entry,
final boolean isAttrValue)
{
if (isDnComponent)
{
if (dnComponentIndex <= dnComponents.size())
{
// convert from 1-based to 0-based
return dnComponents.get(dnComponentIndex - 1).toString();
}
else
{
throw new RuntimeException(
String.format("'%d' is not a valid DN replacement index",
dnComponentIndex));
}
}
Attribute attribute = entry.getAttribute(attributeName);
if (attribute == null)
{
throw new RuntimeException(
String.format("Attribute '%s' does not exist in the entry so " +
"the value cannot be constructed", attributeName));
}
String[] values = attribute.getValues();
if (values.length != 1)
{
throw new RuntimeException(
String.format("Attribute '%s' has %d values in the entry so the " +
"value cannot be constructed since a single value " +
"is required", attributeName, values.length));
}
if (isAttrValue)
{
return getDNValue(values[0]);
}
else
{
return values[0];
}
}
/**
* Return true iff this value is a DN component as opposed to an attribute.
*
* @return true iff this value is a DN component.
*/
boolean isDnComponent()
{
return isDnComponent;
}
/**
* Return the DN component index if this value is a DN component,
* e.g. 1 for {1}.
*
* @return The DN component index.
*/
int getDnComponentIndex()
{
return dnComponentIndex;
}
/**
* Return the attribute name if this value is not a DN component,
* e.g. "givenname" for {givenname}.
*
* @return The attribute name.
*/
String getAttributeName()
{
return attributeName;
}
}
// $1 matches the attribute name
// $2 matches the regular expression to replace
// $3 matches the replacement value of the regular expression
// $4 matches the regex modifiers as follows
// g : replace globally instead of just the first one
// i : CASE_INSENSITIVE
// x : COMMENTS
// s : DOTALL
// m : MULTILINE
// u : UNICODE_CASE
// d : UNIX_LINES
private static Pattern ATTRIBUTE_RE_AND_REPLACEMENT_REGEX =
Pattern.compile("^(" + ATTRIBUTE_ONLY_REGEX +
")\\:/([^/]+)/([^/]*)/([a-zA-Z]*)" + "$");
/**
* A replacement value (either DN component or source attribute) that is run
* through a regular expression with a replacement string to construct the
* value.
*/
private static final class RegExAttributeValueChunk
extends AttributeValueChunk
{
private final String rawAttrRegexReplacement;
private final String rawRegexPattern;
private final Pattern matchingPattern;
private final String replacementString;
private final boolean isReplaceAll;
/**
* Constructor for the regex replacement value.
*
* @param attrRegexReplacement The regular expression replacement.
*/
private RegExAttributeValueChunk(final String attrRegexReplacement)
{
super(attrRegexReplacement.split(":")[0]);
this.rawAttrRegexReplacement = attrRegexReplacement;
Matcher matcher = ATTRIBUTE_RE_AND_REPLACEMENT_REGEX.matcher(
attrRegexReplacement);
if (!matcher.matches())
{
throw new IllegalArgumentException(
String.format("'%s' is an invalid constructed value template",
attrRegexReplacement));
}
String attribute = matcher.group(1);
String regex = matcher.group(2);
String replacement = matcher.group(3);
String flags = matcher.group(4);
boolean replaceAll = false;
int patternFlags = 0;
for (char flag : flags.toCharArray())
{
int flagMask = 0;
switch (flag)
{
case 'i':
flagMask = Pattern.CASE_INSENSITIVE;
break;
case 'x':
flagMask = Pattern.COMMENTS;
break;
case 's':
flagMask = Pattern.DOTALL;
break;
case 'm':
flagMask = Pattern.MULTILINE;
break;
case 'u':
flagMask = Pattern.UNICODE_CASE;
break;
case 'd':
flagMask = Pattern.UNIX_LINES;
break;
case 'g':
if (replaceAll)
{
throw new IllegalArgumentException(
String.format(
"Flag '%c' should only be specified once in '%s'",
flag, attrRegexReplacement));
}
replaceAll = true;
break;
default:
throw new IllegalArgumentException(
String.format("'%c' is not a valid flag in '%s'",
flag, attrRegexReplacement));
}
if ((patternFlags & flagMask) != 0)
{
throw new IllegalArgumentException(
String.format(
"Flag '%c' should only be specified once in '%s'",
flag, attrRegexReplacement));
}
patternFlags |= flagMask;
}
try
{
this.matchingPattern = Pattern.compile(regex, patternFlags);
}
catch (Exception e)
{
Debug.debugException(e);
throw new IllegalArgumentException(
String.format("'%s' is not a valid regular expression: '%s'",
regex, StaticUtils.getExceptionMessage(e)), e);
}
this.rawRegexPattern = regex;
this.replacementString = replacement;
this.isReplaceAll = replaceAll;
}
/**
* {@inheritDoc}
*/
@Override
String getValue(final List dnComponents, final Entry entry,
final boolean isAttrValue)
{
String baseValue = super.getValue(dnComponents, entry, isAttrValue);
Matcher matcher = matchingPattern.matcher(baseValue);
try
{
if (isReplaceAll)
{
return matcher.replaceAll(replacementString);
}
else
{
return matcher.replaceFirst(replacementString);
}
}
catch (Exception e)
{
Debug.debugException(e);
// We might get
throw new RuntimeException(
String.format(
"Could not construct a value using '%s' (%s='%s') because: %s",
rawAttrRegexReplacement, getAttributeName(), baseValue,
StaticUtils.getExceptionMessage(e)));
}
}
}
/**
* Ensures that the given valueString is properly escaped for use as a DN.
* NOTE, this code was taken from the LDAP sdk. It could not be used
* directly, but was modified for use here. It was taken from the RDN
* class.
*
* @param valueString the string value of an attriubte to be used in a DN.
* @return the properly escaped attribute value as a string.
*/
private static String getDNValue(final String valueString) {
int length = valueString.length();
StringBuilder buffer = new StringBuilder(length);
for(int i = 0; i < length; ++i) {
char c = valueString.charAt(i);
switch(c) {
case ' ':
if(i != 0 && i + 1 != length &&
(i + 1 >= length || valueString.charAt(i + 1) != 32)) {
buffer.append(' ');
} else {
buffer.append("\\ ");
}
break;
case '\"':
case '#':
case '+':
case ',':
case ';':
case '<':
case '=':
case '>':
case '\\':
buffer.append('\\');
buffer.append(c);
break;
default:
if(c >= 32 && c <= 126) {
buffer.append(c);
} else {
StaticUtils.hexEncode(c, buffer);
}
}
}
return buffer.toString();
}
// TODO: it might be nice for the Chunks to be aware of the attribute that
// we're constructing a value for. Maybe they could be non-static inner
// classes.
}