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
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,25 @@

package org.apache.jackrabbit.oak.plugins.index;

import org.apache.jackrabbit.oak.spi.state.NodeBuilder;

/**
* Extension to IndexUpdateCallback which also provides access to
* {@link IndexingContext}
* {@link IndexingContext} and the root {@link NodeBuilder} for the current commit.
*/
public interface ContextAwareCallback extends IndexUpdateCallback {

IndexingContext getIndexingContext();

/**
* Returns the root {@link NodeBuilder} for the current commit, allowing
* index editors to write data outside the index definition subtree
* (e.g. to {@code /var/indexing/lucene/<indexName>}).
*
* @return the root NodeBuilder, or {@code null} when not available
* (e.g. in test contexts where a plain mock is used)
*/
default NodeBuilder getRootBuilder() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.index;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Helper for normalizing index definition properties into canonical form.
* Handles backward compatibility with legacy 'type' property while supporting
* new 'storeTargets' and 'activeTarget' properties for multi-target writes.
*/
public class IndexDefinitionHelper {

private static final Logger LOG = LoggerFactory.getLogger(IndexDefinitionHelper.class);

// Constants - these reference oak-search FulltextIndexConstants but are duplicated
// here to avoid circular dependency
private static final String STORE_TARGETS = "storeTargets";
private static final String ACTIVE_TARGET = "activeTarget";
private static final String TYPE = "type";

private IndexDefinitionHelper() {
// Static utility class
}

/**
* Normalize index properties into canonical form with storeTargets and activeTarget.
*
* <p>Normalization rules:</p>
* <ul>
* <li>If storeTargets defined but not activeTarget β†’ ERROR</li>
* <li>If activeTarget defined but not storeTargets β†’ storeTargets = [activeTarget]</li>
* <li>If type only β†’ storeTargets = [type], activeTarget = type</li>
* <li>If both storeTargets/activeTarget defined β†’ use as-is</li>
* <li>If type also defined with storeTargets/activeTarget β†’ log INFO, ignore type</li>
* <li>If activeTarget not in storeTargets β†’ ERROR</li>
* </ul>
*
* @param definition index definition node state
* @return normalized properties with storeTargets and activeTarget
* @throws IllegalArgumentException if validation fails
*/
@NotNull
public static NormalizedIndexProperties normalize(@NotNull NodeState definition) {
PropertyState storeTargetsProperty = definition.getProperty(STORE_TARGETS);
PropertyState activeTargetProperty = definition.getProperty(ACTIVE_TARGET);
PropertyState typeProperty = definition.getProperty(TYPE);

List<String> storeTargets = null;
String activeTarget = null;

// Extract property values if present
if (storeTargetsProperty != null) {
storeTargets = new ArrayList<>();
for (String target : storeTargetsProperty.getValue(Type.STRINGS)) {
storeTargets.add(target);
}
}

if (activeTargetProperty != null) {
activeTarget = activeTargetProperty.getValue(Type.STRING);
}

String type = typeProperty != null ? typeProperty.getValue(Type.STRING) : null;

// Validation: storeTargets requires activeTarget
if (storeTargets != null && activeTarget == null) {
throw new IllegalArgumentException(
"storeTargets requires activeTarget to be set");
}

// Normalization logic
if (storeTargets != null && activeTarget != null) {
// Both defined - use as-is
if (type != null) {
LOG.info("type property '{}' ignored when storeTargets/activeTarget are defined", type);
}
return new NormalizedIndexProperties(storeTargets, activeTarget);

} else if (activeTarget != null) {
// activeTarget only - normalize to storeTargets = [activeTarget]
if (type != null) {
LOG.info("type property '{}' ignored when activeTarget is defined", type);
}
return new NormalizedIndexProperties(Collections.singletonList(activeTarget), activeTarget);

} else if (type != null) {
// type only - normalize to storeTargets = [type], activeTarget = type
return new NormalizedIndexProperties(Collections.singletonList(type), type);

} else {
// None defined - error
throw new IllegalArgumentException(
"Either type or activeTarget must be defined");
}
}

/**
* Get active target for queries (reads activeTarget or falls back to type).
* This is a convenience method that performs normalization internally.
*
* @param definition index definition node state
* @return active target for queries
*/
@NotNull
public static String getActiveTarget(@NotNull NodeState definition) {
return normalize(definition).getActiveTarget();
}

/**
* Get store targets for writes (reads storeTargets or falls back to [type]).
* This is a convenience method that performs normalization internally.
*
* @param definition index definition node state
* @return list of store targets for writes
*/
@NotNull
public static List<String> getStoreTargets(@NotNull NodeState definition) {
return normalize(definition).getStoreTargets();
}

/**
* Returns true if {@code providerType} should write to this index.
*
* <p>If {@code storeTargets} is present, the provider type must appear in the list.
* If absent (legacy {@code type=} only), the provider type must equal {@code type}.</p>
*
* <p>Returns false for invalid definitions (swallows {@link IllegalArgumentException}).</p>
*/
public static boolean shouldWrite(@NotNull NodeState definition, @NotNull String providerType) {
try {
return normalize(definition).getStoreTargets().contains(providerType);
} catch (IllegalArgumentException e) {
return false;
}
}

/**
* Returns true if {@code providerType} should serve queries for this index
* (i.e. {@code activeTarget == providerType}).
*
* <p>Returns false for invalid definitions (swallows {@link IllegalArgumentException}).</p>
*/
public static boolean shouldServeQueries(@NotNull NodeState definition, @NotNull String providerType) {
try {
return providerType.equals(getActiveTarget(definition));
} catch (IllegalArgumentException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.apache.jackrabbit.oak.plugins.index.progress.NodeCountEstimator;
import org.apache.jackrabbit.oak.plugins.index.progress.TraversalRateEstimator;
import org.apache.jackrabbit.oak.plugins.index.upgrade.IndexDisabler;
import org.apache.jackrabbit.oak.plugins.index.IndexDefinitionHelper;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.CompositeEditor;
import org.apache.jackrabbit.oak.spi.commit.Editor;
Expand Down Expand Up @@ -239,14 +240,25 @@ private boolean shouldReindex(NodeBuilder definition, NodeState before, String n
PropertyState type = definition.getProperty(TYPE_PROPERTY_NAME);

// Do not attempt reindex of indexes with no type or disabled
if (type == null || TYPE_DISABLED.equals(type.getValue(Type.STRING))) {
String typeValue;
if (type == null) {
// Support activeTarget-only definitions (no legacy type= property)
try {
typeValue = IndexDefinitionHelper.getActiveTarget(definition.getNodeState());
// valid def with activeTarget β€” fall through to reindex check
} catch (IllegalArgumentException e) {
return false;
}
} else if (TYPE_DISABLED.equals(type.getValue(Type.STRING))) {
return false;
} else {
typeValue = type.getValue(Type.STRING);
}

// Async indexes are not considered for reindexing for sync indexing
// Skip this check for elastic index
// TODO : See if the check to skip elastic can be handled in a better way - maybe move isMatchingIndexNode to IndexDefinition ?
if (!TYPE_ELASTICSEARCH.equals(type.getValue(Type.STRING)) && !isMatchingIndexMode(definition)) {
if (!TYPE_ELASTICSEARCH.equals(typeValue) && !isMatchingIndexMode(definition)) {
return false;
}

Expand All @@ -271,7 +283,7 @@ private boolean shouldReindex(NodeBuilder definition, NodeState before, String n
// someone added the new index node and forgot to add
// the reindex flag, in case OutOfBand Indexing has been performed, warning can be ignored.
// Also, in case the new elastic node has been added with reindex = true , this method would have already returned true
if (result && TYPE_ELASTICSEARCH.equals((type.getValue(Type.STRING)))) {
if (result && TYPE_ELASTICSEARCH.equals(typeValue)) {
log.warn("Found a new elastic index node [{}]. Please set the reindex flag = true to initiate reindexing." +
"Please ignore if OutOfBand Reindexing has already been performed.", name);
return false;
Expand Down Expand Up @@ -306,8 +318,12 @@ private void collectIndexEditors(NodeBuilder definitions, NodeState before) thro
String type = definition.getString(TYPE_PROPERTY_NAME);
String primaryType = definition.getName(JcrConstants.JCR_PRIMARYTYPE);
if (type == null) {
// probably not an index def
continue;
try {
type = IndexDefinitionHelper.getActiveTarget(definition.getNodeState());
} catch (IllegalArgumentException e) {
// not a valid index def
continue;
}
}
/*
Log a warning after every indexJcrTypeInvalidLogLimiter cycles of indexer where nodeState changed.
Expand Down Expand Up @@ -637,6 +653,7 @@ private static final class IndexUpdateRootState {
final IndexEditorProvider provider;
final String async;
final NodeState root;
final NodeBuilder rootBuilder;
final CommitInfo commitInfo;
final IndexDisabler indexDisabler;
private boolean ignoreReindexFlags = IGNORE_REINDEX_FLAGS;
Expand All @@ -654,6 +671,7 @@ private IndexUpdateRootState(IndexEditorProvider provider, String async, NodeSta
this.provider = requireNonNull(provider);
this.async = async;
this.root = requireNonNull(root);
this.rootBuilder = requireNonNull(builder);
this.commitInfo = commitInfo;
this.corruptIndexHandler = corruptIndexHandler;
this.indexDisabler = new IndexDisabler(builder);
Expand Down Expand Up @@ -726,6 +744,11 @@ public IndexingContext getIndexingContext() {
return this;
}

@Override
public NodeBuilder getRootBuilder() {
return IndexUpdateRootState.this.rootBuilder;
}

//~--------------------------------< IndexingContext >

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.index;

import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;

/**
* Immutable holder for normalized index properties (storeTargets and activeTarget).
* Created by {@link IndexDefinitionHelper#normalize} to provide a canonical view
* of index configuration regardless of whether the legacy 'type' property or new
* 'storeTargets'/'activeTarget' properties are used.
*/
public class NormalizedIndexProperties {

private final List<String> storeTargets;
private final String activeTarget;

/**
* Creates normalized index properties.
*
* @param storeTargets list of storage types to write to (never empty)
* @param activeTarget storage type to use for queries (never null, always in storeTargets)
*/
public NormalizedIndexProperties(@NotNull List<String> storeTargets, @NotNull String activeTarget) {
if (storeTargets == null || storeTargets.isEmpty()) {
throw new IllegalArgumentException("storeTargets cannot be null or empty");
}
if (activeTarget == null || activeTarget.isEmpty()) {
throw new IllegalArgumentException("activeTarget cannot be null or empty");
}
if (!storeTargets.contains(activeTarget)) {
throw new IllegalArgumentException(
"activeTarget '" + activeTarget + "' must be in storeTargets " + storeTargets);
}

this.storeTargets = Collections.unmodifiableList(storeTargets);
this.activeTarget = activeTarget;
}

/**
* @return immutable list of storage types to write to (never empty)
*/
@NotNull
public List<String> getStoreTargets() {
return storeTargets;
}

/**
* @return storage type to use for queries (never null, always in storeTargets)
*/
@NotNull
public String getActiveTarget() {
return activeTarget;
}

/**
* @return true if this index writes to multiple targets
*/
public boolean isMultiTarget() {
return storeTargets.size() > 1;
}

@Override
public String toString() {
return "NormalizedIndexProperties{" +
"storeTargets=" + storeTargets +
", activeTarget='" + activeTarget + '\'' +
'}';
}
}
Loading
Loading