Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 61 additions & 58 deletions php/libraries/Candidate.class.inc
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,7 @@ class Candidate implements \LORIS\StudyEntities\AccessibleResource,
* Transforms a config structure (such as in PSCID) into a
* Perl-compatible regex expression for validation
*
* @param array $structure the structure root
* @param string $structure the identifier template
* @param string|null $siteAbbrev the site abbreviation, sometimes used as
* part of the PSCID
* @param string|null $projectAbbrev the project abbreviation
Expand All @@ -1196,76 +1196,79 @@ class Candidate implements \LORIS\StudyEntities\AccessibleResource,
* @cleanup
*/
static function structureToPCRE(
array $structure,
string $structure,
?string $siteAbbrev = null,
?string $projectAbbrev = null
): string {
$seqs = $structure['seq'];
// handle the situation where there exists only one seq
if (isset($seqs['#'])) {
$seqs = [$seqs];
}
$regex = "";
foreach ($seqs AS $seq) {
$unit = "";
$regex = preg_quote($structure, '/');

switch ($seq['@']['type']) {
case 'alpha':
$unit .= '[a-z]';
break;
$regex = str_replace(
preg_quote('{SITE:ALIAS}', '/'),
preg_quote($siteAbbrev ?? '', '/'),
$regex
);

case 'alphanumeric':
$unit .= '[0-9a-z]';
break;
$regex = str_replace(
preg_quote('{PROJECT:ALIAS}', '/'),
preg_quote($projectAbbrev ?? '', '/'),
$regex
);

case 'numeric':
$unit .= '[0-9]';
break;
$escaped = '\\\\';

case 'static':
$unit .= '('.$seq['#'].')';
break;
$regex = preg_replace_callback(
"/{$escaped}\{SEQUENCE{$escaped}:(\d+)([^}]*){$escaped}\}/i",
function ($matches) use ($escaped) {
$characters = '[0-9]';

case 'set':
if (strpos($seq['#'], '|') !== false) {
$unit .= '('.$seq['#'].')';
} else {
$unit .= '['.$seq['#'].']';
if (preg_match(
"/FORMAT{$escaped}:(numeric|alphanumeric|alpha)/i",
$matches[2],
$type
)
) {
switch (strtolower($type[1])) {
case 'alphanumeric':
$characters = '[0-9A-Z]';
break;

case 'alpha':
$characters = '[A-Z]';
break;
}
}
break;

case 'siteAbbrev':
$unit .= $siteAbbrev;
break;
case 'projectAbbrev':
$unit .= $projectAbbrev;
break;
} // end switch

$length = "";
if (isset($seq['@']['length'])) {
$length .= $seq['@']['length'];
} elseif (isset($seq['@']['minLength'])) {
$length .= $seq['@']['minLength'];
} else {
$length .= '1';
}

$length .= ',';
return $characters . '{' . $matches[1] . '}';
},
$regex
);

if (isset($seq['@']['length'])) {
$length .= $seq['@']['length'];
} elseif (isset($seq['@']['maxLength'])) {
$length .= $seq['@']['maxLength'];
} elseif (!isset($seq['@']['length'])
&& !isset($seq['@']['minLength'])
) {
$length .= '1';
}
$regex = preg_replace_callback(
"/{$escaped}\{RANDOM{$escaped}:(\d+)([^}]*){$escaped}\}/i",
function ($matches) use ($escaped) {
$characters = '[0-9]';

$regex .= $unit.'{'.$length.'}';
if (preg_match(
"/FORMAT{$escaped}:(numeric|alphanumeric|alpha)/i",
$matches[2],
$type
)
) {
switch (strtolower($type[1])) {
case 'alphanumeric':
$characters = '[0-9A-Z]';
break;

case 'alpha':
$characters = '[A-Z]';
break;
}
}

} // end foreach seq
return $characters . '{' . $matches[1] . '}';
},
$regex
);

return '/^'.$regex.'$/i';
}
Expand Down
13 changes: 10 additions & 3 deletions php/libraries/IdentifierGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*/
abstract class IdentifierGenerator
{
private const RANGE_FULL = 'Cannot create new identifier because all ' .
private const RANGE_FULL = 'Cannot create new identifier because all' .
' valid identifiers are in use!';
/**
* How the ID will be generated. 'sequential' or 'random'.
Expand Down Expand Up @@ -71,6 +71,13 @@ abstract class IdentifierGenerator
* @var string
*/
protected $prefix;
/**
* The character used to pad the suffix portion of the ID to the
* configured length.
*
* @var string
*/
protected $padding;
/**
* A short name for a Site.
*
Expand Down Expand Up @@ -140,7 +147,7 @@ protected function generateSequentialID(string $id = ''): string
return str_pad(
strval($this->minValue),
$this->length,
"0",
$this->padding,
STR_PAD_LEFT
);
}
Expand Down Expand Up @@ -219,7 +226,7 @@ protected function padID(string $id): string
return str_pad(
$id,
$this->length,
strval($this->alphabet[0]),
$this->padding,
STR_PAD_LEFT
);
}
Expand Down
167 changes: 114 additions & 53 deletions php/libraries/SiteIDGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function __construct(string $siteAlias, string $projectAlias)
$this->generationMethod = $this->_getGeneration();
$this->length = $this->_getLength();
$this->alphabet = $this->_getAlphabet();
$this->padding = $this->_getPadding();
$this->minValue = $this->_getMinValue();
$this->maxValue = $this->_getMaxValue();

Expand Down Expand Up @@ -149,7 +150,7 @@ protected function getExistingIDs(): array
* settings relating to the PSCID structure.
*
* @param string $setting One of: 'generation', 'length', 'alphabet',
* 'min', 'max'.
* 'prefix', 'min', 'max', 'padding'.
*
* @return array<int,int|string>|string|null
*/
Expand All @@ -161,72 +162,113 @@ private function _getIDSetting(

if (!is_array($kind)) {
throw new \LorisException("Invalid config for $this->kind");
};
// The generation setting can be easily extracted and returned.
if ($setting == 'generation') {
return $kind['generation'];
}

// Values other than 'generation' are found within 'seq' elements and
// require more complex processing.
$idStructure = $kind['structure']['seq'];
if (preg_match(
'/\{(SEQUENCE|RANDOM):(\d+)([^}]*)\}/i',
$kind['structure'],
$generation
) !== 1
) {
throw new \ConfigurationException(
"Invalid generation template for {$this->kind}."
);
}

// TODO: Remove this generation consistency check as part of the
// Candidate Identifiers rework.
$expected = $kind['generation'] === 'sequential' ? 'SEQUENCE' : 'RANDOM';

if (!$idStructure[0]) {
// There's only one seq tag so the param format
// needs to be fixed
$temp = [];
$temp[] = $idStructure;
$idStructure = $temp;
if ($kind['generation'] !== 'user'
&& strtoupper($generation[1]) !== $expected
) {
throw new \ConfigurationException("Generation methods do not match.");
}

try {
$seqValue = self::getSeqAttribute($idStructure, $setting);
} catch (\ConfigurationException $e) {
/* Throw a new exception so that we can inform developers whether
* the ConfigurationException arose due to ExternalID or PSCID
* settings.
*/
throw new \LorisException(
"Cannot create new candidate because of a configuration " .
"error in settings for {$this->kind} structure. Details: "
. $e->getMessage()
);
if ($setting === 'generation') {
return $kind['generation'];
}

if ($setting === 'length') {
return $generation[2];
}

if ($setting === 'alphabet') {
switch ($seqValue) {
case 'alpha':
return range('A', 'Z');
case 'numeric':
return range('0', '9');
case 'alphanumeric':
return array_merge(range('0', '9'), range('A', 'Z'));
if (preg_match(
'/(?:^|,)FORMAT:(numeric|alphanumeric|alpha)(?:,|$)/i',
$generation[3],
$match
)
) {
switch (strtolower($match[1])) {
case 'alphanumeric':
return array_merge(
range('0', '9'),
range('A', 'Z')
);

case 'alpha':
return range('A', 'Z');
}
}

return range('0', '9');
}

if ($setting === 'prefix') {
if ($seqValue === 'static') {
// The 'static' seq attribute must also include a value which
// will be a fixed string prefix to be prepended to IDs in
// LORIS. This must be extracted manually.
foreach ($idStructure as $seq) {
if ($seq['@']['type'] === 'static') {
// This index stores the prefix.
return $seq['#'];
}
}
} elseif ($seqValue === 'siteAbbrev') {
return $this->siteAlias;
} elseif ($seqValue === 'projectAbbrev') {
return $this->projectAlias;
} else {
throw new ConfigurationException(
"Incorrect option $seqValue selected for PSCID generation."
$position = strpos(
$kind['structure'],
$generation[0],
);

if ($position === false) {
throw new \ConfigurationException(
"Invalid generation template for {$this->kind}."
);
}

$prefix = substr(
$kind['structure'],
0,
$position
);

$prefix = str_replace(
['{SITE:ALIAS}', '{PROJECT:ALIAS}'],
[$this->siteAlias, $this->projectAlias],
$prefix
);

return $prefix;
}
// The remaining values - min, max, and length - should be returned as
// null if they are not set.
return is_null($seqValue) ? $seqValue: $seqValue;

if ($setting === 'min') {
if (preg_match('/(?:^|,)MIN:([^,]+)/i', $generation[3], $match)) {
return $match[1];
}
return null;
}

if ($setting === 'max') {
if (preg_match('/(?:^|,)MAX:([^,]+)/i', $generation[3], $match)) {
return $match[1];
}
return null;
}

if ($setting === 'padding') {
if (preg_match(
'/(?:^|,)PADDING:([^,]+)/i',
$generation[3],
$match
)
) {
return $match[1];
}
return null;
}

return null;
}

/**
Expand Down Expand Up @@ -425,4 +467,23 @@ private function _getMaxValue(): string
)
);
}

/**
* Returns the padding character for the identifier.
*
* @return string
*/
private function _getPadding(): string
{
$padding = $this->_getIDSetting('padding')
?? strval($this->alphabet[0]);

if (!in_array($padding, $this->alphabet, true)) {
throw new \ConfigurationException(
"Padding character must be part of the configured alphabet."
);
}

return $padding;
}
}
Loading
Loading