001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.connector.federation.merge.strategy;
023    
024    import java.util.HashMap;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.NoSuchElementException;
029    import java.util.UUID;
030    import net.jcip.annotations.ThreadSafe;
031    import org.jboss.dna.connector.federation.contribution.Contribution;
032    import org.jboss.dna.connector.federation.merge.FederatedNode;
033    import org.jboss.dna.connector.federation.merge.MergePlan;
034    import org.jboss.dna.graph.DnaLexicon;
035    import org.jboss.dna.graph.ExecutionContext;
036    import org.jboss.dna.graph.Location;
037    import org.jboss.dna.graph.properties.Name;
038    import org.jboss.dna.graph.properties.Path;
039    import org.jboss.dna.graph.properties.PathFactory;
040    import org.jboss.dna.graph.properties.Property;
041    import org.jboss.dna.graph.properties.PropertyFactory;
042    import org.jboss.dna.graph.properties.ValueComparators;
043    
044    /**
045     * This merge strategy simply merges all of the contributions' properties and combines the children according to the order of the
046     * contributions. No children are merged, and all properties are used (except if they are deemed to be duplicates of the property
047     * in other contributions).
048     * 
049     * @author Randall Hauch
050     */
051    @ThreadSafe
052    public class SimpleMergeStrategy implements MergeStrategy {
053    
054        private boolean removeDuplicateProperties = true;
055    
056        /**
057         * @return removeDuplicateProperties
058         */
059        public boolean isRemoveDuplicateProperties() {
060            return removeDuplicateProperties;
061        }
062    
063        /**
064         * @param removeDuplicateProperties Sets removeDuplicateProperties to the specified value.
065         */
066        public void setRemoveDuplicateProperties( boolean removeDuplicateProperties ) {
067            this.removeDuplicateProperties = removeDuplicateProperties;
068        }
069    
070        /**
071         * {@inheritDoc}
072         * 
073         * @see org.jboss.dna.connector.federation.merge.strategy.MergeStrategy#merge(org.jboss.dna.connector.federation.merge.FederatedNode,
074         *      java.util.List, org.jboss.dna.graph.ExecutionContext)
075         */
076        public void merge( FederatedNode federatedNode,
077                           List<Contribution> contributions,
078                           ExecutionContext context ) {
079            assert federatedNode != null;
080            assert context != null;
081            assert contributions != null;
082            assert contributions.size() > 0;
083    
084            final Location location = federatedNode.getActualLocationOfNode();
085            final boolean isRoot = location.hasPath() && location.getPath().isRoot();
086            final boolean removeDuplicateProperties = isRemoveDuplicateProperties();
087            final PathFactory pathFactory = context.getValueFactories().getPathFactory();
088            final Map<Name, Integer> childNames = new HashMap<Name, Integer>();
089            final Map<Name, Property> properties = federatedNode.getPropertiesByName();
090            properties.clear();
091    
092            // Record the different ID properties from the federated node and (later) from each contribution
093            final Map<Name, Property> idProperties = new HashMap<Name, Property>();
094            for (Property idProperty : location) {
095                idProperties.put(idProperty.getName(), idProperty);
096            }
097    
098            // Iterate over each of the contributions (in order) ...
099            for (Contribution contribution : contributions) {
100                if (contribution.isEmpty()) continue;
101                // If the contribution is a placeholder contribution, then the children should be merged into other children ...
102                if (contribution.isPlaceholder()) {
103                    // Iterate over the children and add only if there is not already one ...
104                    Iterator<Location> childIterator = contribution.getChildren();
105                    while (childIterator.hasNext()) {
106                        Location child = childIterator.next();
107                        Name childName = child.getPath().getLastSegment().getName();
108                        if (!childNames.containsKey(childName)) {
109                            childNames.put(childName, 1);
110                            Path pathToChild = pathFactory.create(location.getPath(), childName);
111                            federatedNode.addChild(new Location(pathToChild));
112                        }
113                    }
114                } else {
115                    // Get the identification properties for each contribution ...
116                    Location contributionLocation = contribution.getLocationInSource();
117                    for (Property idProperty : contributionLocation) {
118                        // Record the property ...
119                        Property existing = properties.put(idProperty.getName(), idProperty);
120                        if (existing != null) {
121                            // There's already an existing property, so we need to merge them ...
122                            Property merged = merge(existing, idProperty, context.getPropertyFactory(), removeDuplicateProperties);
123                            properties.put(merged.getName(), merged);
124                        }
125                    }
126    
127                    // Accumulate the children ...
128                    Iterator<Location> childIterator = contribution.getChildren();
129                    while (childIterator.hasNext()) {
130                        Location child = childIterator.next();
131                        Name childName = child.getPath().getLastSegment().getName();
132                        int index = Path.NO_INDEX;
133                        Integer previous = childNames.put(childName, 1);
134                        if (previous != null) {
135                            int previousValue = previous.intValue();
136                            // Correct the index in the child name map ...
137                            childNames.put(childName, ++previousValue);
138                            index = previousValue;
139                        }
140                        Path pathToChild = pathFactory.create(location.getPath(), childName, index);
141                        federatedNode.addChild(new Location(pathToChild));
142                    }
143    
144                    // Add in the properties ...
145                    Iterator<Property> propertyIter = contribution.getProperties();
146                    while (propertyIter.hasNext()) {
147                        Property property = propertyIter.next();
148                        // Skip the "uuid" property on all root nodes ...
149                        if (isRoot && property.getName().getLocalName().equals("uuid")) continue;
150    
151                        // Record the property ...
152                        Property existing = properties.put(property.getName(), property);
153                        if (existing != null) {
154                            // There's already an existing property, so we need to merge them ...
155                            Property merged = merge(existing, property, context.getPropertyFactory(), removeDuplicateProperties);
156                            properties.put(property.getName(), merged);
157                        }
158                    }
159                }
160            }
161    
162            if (idProperties.size() != 0) {
163                // Update the location based upon the merged ID properties ...
164                Location newLocation = new Location(location.getPath(), idProperties.values());
165                federatedNode.setActualLocationOfNode(newLocation);
166    
167                // Look for the UUID property on the location, and update the federated node ...
168                Property uuidProperty = idProperties.get(DnaLexicon.UUID);
169                if (uuidProperty != null && !uuidProperty.isEmpty()) {
170                    UUID uuid = context.getValueFactories().getUuidFactory().create(uuidProperty.getValues().next());
171                    federatedNode.setUuid(uuid);
172    
173                    // Set the UUID as a property ...
174                    properties.put(uuidProperty.getName(), uuidProperty);
175                }
176            } else {
177                // Generate a new UUID property and add to the node ...
178                UUID uuid = federatedNode.getUuid();
179                if (uuid == null) {
180                    uuid = context.getValueFactories().getUuidFactory().create();
181                    federatedNode.setUuid(uuid);
182                }
183    
184                // Set the UUID as a property ...
185                Property uuidProperty = context.getPropertyFactory().create(DnaLexicon.UUID, uuid);
186                properties.put(uuidProperty.getName(), uuidProperty);
187            }
188    
189            // Assign the merge plan ...
190            MergePlan mergePlan = MergePlan.create(contributions);
191            federatedNode.setMergePlan(mergePlan);
192        }
193    
194        /**
195         * Merge the values from the two properties with the same name, returning a new property with the newly merged values.
196         * <p>
197         * The current algorithm merges the values by concatenating the values from <code>property1</code> and <code>property2</code>,
198         * and if <code>removeDuplicates</code> is true any values in <code>property2</code> that are identical to values found in
199         * <code>property1</code> are skipped.
200         * </p>
201         * 
202         * @param property1 the first property; may not be null, and must have the same {@link Property#getName() name} as
203         *        <code>property2</code>
204         * @param property2 the second property; may not be null, and must have the same {@link Property#getName() name} as
205         *        <code>property1</code>
206         * @param factory the property factory, used to create the result
207         * @param removeDuplicates true if this method removes any values in the second property that duplicate values found in the
208         *        first property.
209         * @return the property that contains the same {@link Property#getName() name} as the input properties, but with values that
210         *         are merged from both of the input properties
211         */
212        protected Property merge( Property property1,
213                                  Property property2,
214                                  PropertyFactory factory,
215                                  boolean removeDuplicates ) {
216            assert property1 != null;
217            assert property2 != null;
218            assert property1.getName().equals(property2.getName());
219            if (property1.isEmpty()) return property2;
220            if (property2.isEmpty()) return property1;
221    
222            // If they are both single-valued, then we can use a more efficient algorithm ...
223            if (property1.isSingle() && property2.isSingle()) {
224                Object value1 = property1.getValues().next();
225                Object value2 = property2.getValues().next();
226                if (removeDuplicates && ValueComparators.OBJECT_COMPARATOR.compare(value1, value2) == 0) return property1;
227                return factory.create(property1.getName(), new Object[] {value1, value2});
228            }
229    
230            // One or both properties are multi-valued, so use an algorithm that works with in all cases ...
231            if (!removeDuplicates) {
232                Iterator<?> valueIterator = new DualIterator(property1.getValues(), property2.getValues());
233                return factory.create(property1.getName(), valueIterator);
234            }
235    
236            // First copy all the values from property 1 ...
237            Object[] values = new Object[property1.size() + property2.size()];
238            int index = 0;
239            for (Object property1Value : property1) {
240                values[index++] = property1Value;
241            }
242            assert index == property1.size();
243            // Now add any values of property2 that don't match a value in property1 ...
244            for (Object property2Value : property2) {
245                // Brute force, go through the values of property1 and compare ...
246                boolean matched = false;
247                for (Object property1Value : property1) {
248                    if (ValueComparators.OBJECT_COMPARATOR.compare(property1Value, property2Value) == 0) {
249                        // The values are the same ...
250                        matched = true;
251                        break;
252                    }
253                }
254                if (!matched) values[index++] = property2Value;
255            }
256            if (index != values.length) {
257                Object[] newValues = new Object[index];
258                System.arraycopy(values, 0, newValues, 0, index);
259                values = newValues;
260            }
261            return factory.create(property1.getName(), values);
262        }
263    
264        protected static class DualIterator implements Iterator<Object> {
265    
266            private final Iterator<?>[] iterators;
267            private Iterator<?> current;
268            private int index = 0;
269    
270            protected DualIterator( Iterator<?>... iterators ) {
271                assert iterators != null;
272                assert iterators.length > 0;
273                this.iterators = iterators;
274                this.current = this.iterators[0];
275            }
276    
277            /**
278             * {@inheritDoc}
279             * 
280             * @see java.util.Iterator#hasNext()
281             */
282            public boolean hasNext() {
283                if (this.current != null) return this.current.hasNext();
284                return false;
285            }
286    
287            /**
288             * {@inheritDoc}
289             * 
290             * @see java.util.Iterator#next()
291             */
292            public Object next() {
293                while (this.current != null) {
294                    if (this.current.hasNext()) return this.current.next();
295                    // Get the next iterator ...
296                    if (++this.index < iterators.length) {
297                        this.current = this.iterators[this.index];
298                    } else {
299                        this.current = null;
300                    }
301                }
302                throw new NoSuchElementException();
303            }
304    
305            /**
306             * {@inheritDoc}
307             * 
308             * @see java.util.Iterator#remove()
309             */
310            public void remove() {
311                throw new UnsupportedOperationException();
312            }
313        }
314    
315        protected static class Child {
316            private final Location location;
317            private final boolean placeholder;
318    
319            protected Child( Location location,
320                             boolean placeholder ) {
321                assert location != null;
322                this.location = location;
323                this.placeholder = placeholder;
324            }
325    
326            /**
327             * {@inheritDoc}
328             * 
329             * @see java.lang.Object#hashCode()
330             */
331            @Override
332            public int hashCode() {
333                return location.hasIdProperties() ? location.getIdProperties().hashCode() : location.getPath().getLastSegment().getName().hashCode();
334            }
335    
336            /**
337             * {@inheritDoc}
338             * 
339             * @see java.lang.Object#equals(java.lang.Object)
340             */
341            @Override
342            public boolean equals( Object obj ) {
343                if (obj == this) return true;
344                if (obj instanceof Child) {
345                    Child that = (Child)obj;
346                    if (this.placeholder && that.placeholder) {
347                        // If both are placeholders, then compare just the name ...
348                        assert this.location.hasPath();
349                        assert that.location.hasPath();
350                        Name thisName = this.location.getPath().getLastSegment().getName();
351                        Name thatName = that.location.getPath().getLastSegment().getName();
352                        return thisName.equals(thatName);
353                    }
354                    if (location.hasIdProperties() && that.location.hasIdProperties()) {
355                        List<Property> thisIds = location.getIdProperties();
356                        List<Property> thatIds = that.location.getIdProperties();
357                        if (thisIds.size() != thatIds.size()) return false;
358                        return thisIds.containsAll(thatIds);
359                    }
360                    // One or both do not have identification properties, so delegate to the locations...
361                    return this.location.equals(that.location);
362                }
363                return false;
364            }
365    
366            /**
367             * {@inheritDoc}
368             * 
369             * @see java.lang.Object#toString()
370             */
371            @Override
372            public String toString() {
373                return location.toString();
374            }
375        }
376    }