| /* |
| * 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.upgrade.security; |
| |
| import java.util.Set; |
| import java.util.TreeSet; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder; |
| import org.apache.jackrabbit.oak.security.user.MembershipWriter; |
| import org.apache.jackrabbit.oak.spi.commit.DefaultEditor; |
| import org.apache.jackrabbit.oak.spi.commit.Editor; |
| import org.apache.jackrabbit.oak.spi.security.user.UserConstants; |
| import org.apache.jackrabbit.oak.spi.state.NodeBuilder; |
| import org.apache.jackrabbit.oak.spi.state.NodeState; |
| import org.apache.jackrabbit.util.Text; |
| import org.jetbrains.annotations.NotNull; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT; |
| import static org.apache.jackrabbit.oak.api.Type.NAME; |
| |
| class GroupEditor extends DefaultEditor { |
| |
| private static final Logger log = LoggerFactory.getLogger(GroupEditor.class); |
| |
| private final String[] groupsRoot; |
| |
| private State state; |
| |
| private EditorGroup currentGroup; |
| |
| private final UpgradeMembershipWriter writer = new UpgradeMembershipWriter(); |
| |
| GroupEditor(@NotNull NodeBuilder builder, @NotNull String groupsPath) { |
| this.state = new State(builder); |
| this.groupsRoot = Text.explode(groupsPath, '/', false); |
| // writer.setMembershipSizeThreshold(10); // uncomment to test different split sizes |
| } |
| |
| private boolean descend(String name) { |
| if (state.depth < groupsRoot.length && !name.equals(groupsRoot[state.depth])) { |
| return false; |
| } |
| state = state.push(name); |
| return true; |
| } |
| |
| private void ascend() { |
| state = state.pop(); |
| } |
| |
| @Override |
| public void leave(NodeState before, NodeState after) throws CommitFailedException { |
| if (currentGroup != null && currentGroup.path.equals(state.path)) { |
| log.info("scanned group {}, {} members", currentGroup.path, currentGroup.members.size()); |
| writer.setMembers(state.builder, currentGroup.members); |
| currentGroup = null; |
| } |
| ascend(); |
| } |
| |
| @SuppressWarnings("deprecation") |
| @Override |
| public Editor childNodeAdded(String name, NodeState after) throws CommitFailedException { |
| if (!descend(name)) { |
| return null; |
| } |
| |
| String nt = after.getName(JCR_PRIMARYTYPE); |
| if (nt == null) { |
| throw new CommitFailedException( |
| CONSTRAINT, 34, JCR_PRIMARYTYPE + " missing at " + state.path); |
| } |
| |
| if (UserConstants.NT_REP_GROUP.equals(nt)) { |
| if (currentGroup != null) { |
| log.error("rep:Group within rep:Group not supported during upgrade. current group: {}, overwriting group: {}", |
| currentGroup.path, state.path); |
| } |
| currentGroup = new EditorGroup(state.path); |
| currentGroup.addMembers(after.getProperty(UserConstants.REP_MEMBERS)); |
| } else if (UserConstants.NT_REP_MEMBERS.equals(nt)) { |
| if (currentGroup == null) { |
| log.warn("rep:Members detected outside of a rep:Group. ignoring {}", state.path); |
| } else { |
| currentGroup.addMembers(after); |
| } |
| } |
| return this; |
| } |
| |
| @Override |
| public Editor childNodeChanged(String name, NodeState before, NodeState after) { |
| return null; |
| } |
| |
| @Override |
| public Editor childNodeDeleted(String name, NodeState before) { |
| throw new IllegalStateException("deleted node during upgrade copy not expected: " + state.path + "/" + name); |
| } |
| |
| private static class State { |
| |
| private final State parent; |
| |
| private final String path; |
| |
| private final NodeBuilder builder; |
| |
| private final int depth; |
| |
| private State(State parent, String name) { |
| this.parent = parent; |
| this.path = parent.path + "/" + name; |
| this.builder = parent.builder.child(name); |
| this.depth = parent.depth + 1; |
| log.debug("{} --> {}", depth, path); |
| } |
| |
| private State(NodeBuilder builder) { |
| this.parent = null; |
| this.path = ""; |
| this.builder = builder; |
| this.depth = 0; |
| } |
| |
| private State push(String name) { |
| return new State(this, name); |
| } |
| |
| private State pop() { |
| log.debug("{} <-- {}", depth, path); |
| return parent; |
| } |
| } |
| |
| private static class EditorGroup { |
| |
| private final String path; |
| |
| private final Set<String> members = new TreeSet<String>(); |
| |
| private EditorGroup(String path) { |
| this.path = path; |
| } |
| |
| private void addMembers(PropertyState repMembers) { |
| if (repMembers != null) { |
| for (String ref: repMembers.getValue(Type.WEAKREFERENCES)) { |
| members.add(ref); |
| } |
| } |
| } |
| |
| private void addMembers(NodeState node) { |
| for (PropertyState prop: node.getProperties()) { |
| if (prop.getType() == Type.WEAKREFERENCE) { |
| members.add(prop.getValue(Type.WEAKREFERENCE)); |
| } |
| } |
| } |
| } |
| |
| private static class UpgradeMembershipWriter { |
| |
| /** |
| * Sets the given set of members to the specified group. this method is only used by the migration code. |
| * |
| * @param group node builder of group |
| * @param members set of content ids to set |
| */ |
| public void setMembers(@NotNull NodeBuilder group, @NotNull Set<String> members) { |
| group.removeProperty(UserConstants.REP_MEMBERS); |
| if (group.hasChildNode(UserConstants.REP_MEMBERS)) { |
| group.getChildNode(UserConstants.REP_MEMBERS).remove(); |
| } |
| |
| PropertyBuilder<String> prop = null; |
| NodeBuilder refList = null; |
| NodeBuilder node = group; |
| |
| int count = 0; |
| int numNodes = 0; |
| for (String ref : members) { |
| if (prop == null) { |
| prop = PropertyBuilder.array(Type.WEAKREFERENCE, UserConstants.REP_MEMBERS); |
| } |
| prop.addValue(ref); |
| count++; |
| if (count > MembershipWriter.DEFAULT_MEMBERSHIP_THRESHOLD) { |
| node.setProperty(prop.getPropertyState()); |
| prop = null; |
| if (refList == null) { |
| // create intermediate structure |
| refList = group.child(UserConstants.REP_MEMBERS_LIST); |
| refList.setProperty(JCR_PRIMARYTYPE, UserConstants.NT_REP_MEMBER_REFERENCES_LIST, NAME); |
| } |
| node = refList.child(String.valueOf(numNodes++)); |
| node.setProperty(JCR_PRIMARYTYPE, UserConstants.NT_REP_MEMBER_REFERENCES, NAME); |
| } |
| } |
| if (prop != null) { |
| node.setProperty(prop.getPropertyState()); |
| } |
| } |
| } |
| } |