001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.directory.server.core.entry;
020    
021    import java.util.ArrayList;
022    import java.util.HashSet;
023    import java.util.List;
024    import java.util.NoSuchElementException;
025    import java.util.Set;
026    
027    import javax.naming.NamingEnumeration;
028    import javax.naming.NamingException;
029    import javax.naming.directory.Attribute;
030    import javax.naming.directory.Attributes;
031    import javax.naming.directory.BasicAttribute;
032    import javax.naming.directory.BasicAttributes;
033    import javax.naming.directory.DirContext;
034    import javax.naming.directory.InvalidAttributeIdentifierException;
035    import javax.naming.directory.ModificationItem;
036    import javax.naming.directory.SearchResult;
037    
038    import org.apache.directory.server.i18n.I18n;
039    import org.apache.directory.shared.ldap.constants.SchemaConstants;
040    import org.apache.directory.shared.ldap.entry.DefaultServerAttribute;
041    import org.apache.directory.shared.ldap.entry.DefaultServerEntry;
042    import org.apache.directory.shared.ldap.entry.EntryAttribute;
043    import org.apache.directory.shared.ldap.entry.Modification;
044    import org.apache.directory.shared.ldap.entry.ModificationOperation;
045    import org.apache.directory.shared.ldap.entry.ServerEntry;
046    import org.apache.directory.shared.ldap.entry.ServerModification;
047    import org.apache.directory.shared.ldap.entry.Value;
048    import org.apache.directory.shared.ldap.exception.LdapException;
049    import org.apache.directory.shared.ldap.exception.LdapInvalidAttributeTypeException;
050    import org.apache.directory.shared.ldap.name.DN;
051    import org.apache.directory.shared.ldap.schema.AttributeType;
052    import org.apache.directory.shared.ldap.schema.SchemaManager;
053    import org.apache.directory.shared.ldap.schema.SchemaUtils;
054    import org.apache.directory.shared.ldap.util.EmptyEnumeration;
055    import org.apache.directory.shared.ldap.util.StringTools;
056    
057    /**
058     * A helper class used to manipulate Entries, Attributes and Values.
059     *
060     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
061     * @version $Rev$, $Date$
062     */
063    public class ServerEntryUtils
064    {
065        /**
066         * Convert a ServerAttribute into a BasicAttribute. The DN is lost
067         * during this conversion, as the Attributes object does not store
068         * this element.
069         *
070         * @return An instance of a AttributesImpl() object
071         */
072        public static Attribute toBasicAttribute( EntryAttribute entryAttribute )
073        {
074            AttributeType attributeType = entryAttribute.getAttributeType();
075            
076            Attribute attribute = new BasicAttribute( attributeType.getName() );
077            
078            for ( Value<?> value: entryAttribute )
079            {
080                attribute.add( value.get() );
081            }
082            
083            return attribute;
084        }
085        
086        
087        /**
088         * Convert a ServerEntry into a BasicAttributes. The DN is lost
089         * during this conversion, as the Attributes object does not store
090         * this element.
091         *
092         * @return An instance of a AttributesImpl() object
093         */
094        public static Attributes toBasicAttributes( ServerEntry entry )
095        {
096            if ( entry == null )
097            {
098                return null;
099            }
100            
101            Attributes attributes = new BasicAttributes( true );
102    
103            for ( AttributeType attributeType:entry.getAttributeTypes() )
104            {
105                EntryAttribute attr = entry.get( attributeType );
106                
107                // Deal with a special case : an entry without any ObjectClass
108                if ( attributeType.getOid().equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
109                {
110                    if ( attr.size() == 0 )
111                    {
112                        // We don't have any objectClass, just dismiss this element
113                        continue;
114                    }
115                }
116                
117                attributes.put( toBasicAttribute( attr ) );
118            }
119            
120            return attributes;
121        }
122    
123    
124        /**
125         * Convert a BasicAttribute or a AttributeImpl to a ServerAtribute
126         *
127         * @param attribute the BasicAttributes or AttributesImpl instance to convert
128         * @param attributeType
129         * @return An instance of a ServerEntry object
130         * 
131         * @throws InvalidAttributeIdentifierException If we had an incorrect attribute
132         */
133        public static EntryAttribute toServerAttribute( Attribute attribute, AttributeType attributeType )
134        {
135            if ( attribute == null )
136            {
137                return null;
138            }
139            
140            try 
141            {
142                EntryAttribute serverAttribute = new DefaultServerAttribute( attributeType );
143            
144                for ( NamingEnumeration<?> values = attribute.getAll(); values.hasMoreElements(); )
145                {
146                    Object value = values.nextElement();
147                    
148                    if ( value == null )
149                    {
150                        continue;
151                    }
152                    
153                    if ( serverAttribute.isHR() )
154                    {
155                        if ( value instanceof String )
156                        {
157                            serverAttribute.add( (String)value );
158                        }
159                        else if ( value instanceof byte[] )
160                        {
161                            serverAttribute.add( StringTools.utf8ToString( (byte[])value ) );
162                        }
163                        else
164                        {
165                            return null;
166                        }
167                    }
168                    else
169                    {
170                        if ( value instanceof String )
171                        {
172                            serverAttribute.add( StringTools.getBytesUtf8( (String)value ) );
173                        }
174                        else if ( value instanceof byte[] )
175                        {
176                            serverAttribute.add( (byte[])value );
177                        }
178                        else
179                        {
180                            return null;
181                        }
182                    }
183                }
184                
185                return serverAttribute;
186            }
187            catch ( NamingException ne )
188            {
189                return null;
190            }
191        }
192    
193    
194        /**
195         * Convert a BasicAttributes or a AttributesImpl to a ServerEntry
196         *
197         * @param attributes the BasicAttributes or AttributesImpl instance to convert
198         * @param registries The registries, needed ro build a ServerEntry
199         * @param dn The DN which is needed by the ServerEntry 
200         * @return An instance of a ServerEntry object
201         * 
202         * @throws LdapInvalidAttributeTypeException If we get an invalid attribute
203         */
204        public static ServerEntry toServerEntry( Attributes attributes, DN dn, SchemaManager schemaManager ) 
205                throws LdapInvalidAttributeTypeException
206        {
207            if ( attributes instanceof BasicAttributes )
208            {
209                try 
210                {
211                    ServerEntry entry = new DefaultServerEntry( schemaManager, dn );
212        
213                    for ( NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMoreElements(); )
214                    {
215                        Attribute attr = attrs.nextElement();
216    
217                        String attributeId = attr.getID();
218                        String id = SchemaUtils.stripOptions( attributeId );
219                        Set<String> options = SchemaUtils.getOptions( attributeId );
220                        // TODO : handle options.
221                        AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( id );
222                        EntryAttribute serverAttribute = ServerEntryUtils.toServerAttribute( attr, attributeType );
223                        
224                        if ( serverAttribute != null )
225                        {
226                            entry.put( serverAttribute );
227                        }
228                    }
229                    
230                    return entry;
231                }
232                catch ( LdapException ne )
233                {
234                    throw new LdapInvalidAttributeTypeException( ne.getLocalizedMessage() );
235                }
236            }
237            else
238            {
239                return null;
240            }
241        }
242    
243    
244        /**
245         * Gets the target entry as it would look after a modification operation 
246         * was performed on it.
247         * 
248         * @param mod the modification
249         * @param entry the source entry that is modified
250         * @return the resultant entry after the modification has taken place
251         * @throws LdapException if there are problems accessing attributes
252         */
253        public static ServerEntry getTargetEntry( Modification mod, ServerEntry entry, SchemaManager schemaManager ) throws LdapException
254        {
255            ServerEntry targetEntry = ( ServerEntry ) entry.clone();
256            ModificationOperation modOp = mod.getOperation();
257            String id = mod.getAttribute().getId();
258            AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( id );
259            
260            switch ( modOp )
261            {
262                case REPLACE_ATTRIBUTE :
263                    targetEntry.put( mod.getAttribute() );
264                    break;
265                    
266                case REMOVE_ATTRIBUTE :
267                    EntryAttribute toBeRemoved = mod.getAttribute();
268    
269                    if ( toBeRemoved.size() == 0 )
270                    {
271                        targetEntry.removeAttributes( id );
272                    }
273                    else
274                    {
275                        EntryAttribute existing = targetEntry.get( id );
276    
277                        if ( existing != null )
278                        {
279                            for ( Value<?> value:toBeRemoved )
280                            {
281                                existing.remove( value );
282                            }
283                        }
284                    }
285                    break;
286                    
287                case ADD_ATTRIBUTE :
288                    EntryAttribute combined = new DefaultServerAttribute( id, attributeType );
289                    EntryAttribute toBeAdded = mod.getAttribute();
290                    EntryAttribute existing = entry.get( id );
291    
292                    if ( existing != null )
293                    {
294                        for ( Value<?> value:existing )
295                        {
296                            combined.add( value );
297                        }
298                    }
299    
300                    for ( Value<?> value:toBeAdded )
301                    {
302                        combined.add( value );
303                    }
304                    
305                    targetEntry.put( combined );
306                    break;
307                    
308                default:
309                    throw new IllegalStateException( I18n.err( I18n.ERR_464, modOp ) );
310            }
311    
312            return targetEntry;
313        }
314    
315    
316        /**
317         * Creates a new attribute which contains the values representing the union
318         * of two attributes. If one attribute is null then the resultant attribute
319         * returned is a copy of the non-null attribute. If both are null then we
320         * cannot determine the attribute ID and an {@link IllegalArgumentException}
321         * is raised.
322         * 
323         * @param attr0 the first attribute
324         * @param attr1 the second attribute
325         * @return a new attribute with the union of values from both attribute
326         *         arguments
327         * @throws LdapException if there are problems accessing attribute values
328         */
329        public static EntryAttribute getUnion( EntryAttribute attr0, EntryAttribute attr1 )
330        {
331            if ( attr0 == null && attr1 == null )
332            {
333                throw new IllegalArgumentException( I18n.err( I18n.ERR_465 ) );
334            }
335            else if ( attr0 == null )
336            {
337                return attr1.clone();
338            }
339            else if ( attr1 == null )
340            {
341                return attr0.clone();
342            }
343            else if ( !attr0.getAttributeType().equals( attr1.getAttributeType() ) )
344            {
345                throw new IllegalArgumentException( I18n.err( I18n.ERR_466 ) );
346            }
347    
348            EntryAttribute attr = attr0.clone();
349    
350            for ( Value<?> value:attr1 )
351            {
352                attr.add( value );
353            }
354    
355            return attr;
356        }
357        
358        
359        /**
360         * Convert a ModificationItem to an instance of a ServerModification object
361         *
362         * @param modificationImpl the modification instance to convert
363         * @param attributeType the associated attributeType
364         * @return a instance of a ServerModification object
365         */
366        private static Modification toServerModification( ModificationItem modificationImpl, AttributeType attributeType ) 
367        {
368            ModificationOperation operation;
369            
370            switch ( modificationImpl.getModificationOp() )
371            {
372                case DirContext.REMOVE_ATTRIBUTE :
373                    operation = ModificationOperation.REMOVE_ATTRIBUTE;
374                    break;
375                    
376                case DirContext.REPLACE_ATTRIBUTE :
377                    operation = ModificationOperation.REPLACE_ATTRIBUTE;
378                    break;
379    
380                case DirContext.ADD_ATTRIBUTE :
381                default :
382                    operation = ModificationOperation.ADD_ATTRIBUTE;
383                    break;
384                    
385            }
386            
387            Modification modification = new ServerModification( 
388                operation,
389                ServerEntryUtils.toServerAttribute( modificationImpl.getAttribute(), attributeType ) ); 
390            
391            return modification;
392            
393        }
394    
395        
396        /**
397         * 
398         * Convert a list of ModificationItemImpl to a list of 
399         *
400         * @param modificationImpls
401         * @param atRegistry
402         * @return
403         * @throws LdapException
404         */
405        public static List<Modification> convertToServerModification( List<ModificationItem> modificationItems, 
406            SchemaManager schemaManager ) throws LdapException
407        {
408            if ( modificationItems != null )
409            {
410                List<Modification> modifications = new ArrayList<Modification>( modificationItems.size() );
411    
412                for ( ModificationItem modificationItem: modificationItems )
413                {
414                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( modificationItem.getAttribute().getID() );
415                    modifications.add( toServerModification( modificationItem, attributeType ) );
416                }
417            
418                return modifications;
419            }
420            else
421            {
422                return null;
423            }
424        }
425        
426        
427        /**
428         * Convert a Modification to an instance of a ServerModification object.
429         *
430         * @param modificationImpl the modification instance to convert
431         * @param attributeType the associated attributeType
432         * @return a instance of a ServerModification object
433         */
434        private static Modification toServerModification( Modification modification, AttributeType attributeType ) 
435        {
436            if ( modification instanceof ServerModification )
437            {
438                return modification;
439            }
440            
441            Modification serverModification = new ServerModification( 
442                modification.getOperation(),
443                new DefaultServerAttribute( attributeType, modification.getAttribute() ) ); 
444            
445            return serverModification;
446            
447        }
448    
449        
450        public static List<Modification> toServerModification( Modification[] modifications, 
451            SchemaManager schemaManager ) throws LdapException
452        {
453            if ( modifications != null )
454            {
455                List<Modification> modificationsList = new ArrayList<Modification>();
456        
457                for ( Modification modification: modifications )
458                {
459                    String attributeId = modification.getAttribute().getId();
460                    String id = stripOptions( attributeId );
461                    modification.getAttribute().setId( id );
462                    Set<String> options = getOptions( attributeId );
463    
464                    // -------------------------------------------------------------------
465                    // DIRSERVER-646 Fix: Replacing an unknown attribute with no values 
466                    // (deletion) causes an error
467                    // -------------------------------------------------------------------
468                    if ( ! schemaManager.getAttributeTypeRegistry().contains( id ) 
469                         && modification.getAttribute().size() == 0 
470                         && modification.getOperation() == ModificationOperation.REPLACE_ATTRIBUTE )
471                    {
472                        // The attributeType does not exist in the schema.
473                        // It's an error
474                        String message = I18n.err( I18n.ERR_467, id );
475                        throw new LdapInvalidAttributeTypeException( message );
476                    }
477                    else
478                    {
479                        // TODO : handle options
480                        AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( id );
481                        modificationsList.add( toServerModification( modification, attributeType ) );
482                    }
483                }
484            
485                return modificationsList;
486            }
487            else
488            {
489                return null;
490            }
491        }
492    
493    
494        public static List<Modification> toServerModification( ModificationItem[] modifications, 
495            SchemaManager schemaManager ) throws LdapException
496        {
497            if ( modifications != null )
498            {
499                List<Modification> modificationsList = new ArrayList<Modification>();
500        
501                for ( ModificationItem modification: modifications )
502                {
503                    String attributeId = modification.getAttribute().getID();
504                    String id = stripOptions( attributeId );
505                    Set<String> options = getOptions( attributeId );
506    
507                    // -------------------------------------------------------------------
508                    // DIRSERVER-646 Fix: Replacing an unknown attribute with no values 
509                    // (deletion) causes an error
510                    // -------------------------------------------------------------------
511                    
512                    // TODO - after removing JNDI we need to make the server handle 
513                    // this in the codec
514                    
515                    if ( ! schemaManager.getAttributeTypeRegistry().contains( id ) 
516                         && modification.getAttribute().size() == 0 
517                         && modification.getModificationOp() == DirContext.REPLACE_ATTRIBUTE )
518                    {
519                        continue;
520                    }
521    
522                    // -------------------------------------------------------------------
523                    // END DIRSERVER-646 Fix
524                    // -------------------------------------------------------------------
525                    
526                    
527                    // TODO : handle options
528                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( id );
529                    modificationsList.add( toServerModification( (ModificationItem)modification, attributeType ) );
530                }
531            
532                return modificationsList;
533            }
534            else
535            {
536                return null;
537            }
538        }
539    
540    
541        /**
542         * Utility method to extract a modification item from an array of modifications.
543         * 
544         * @param mods the array of ModificationItems to extract the Attribute from.
545         * @param type the attributeType spec of the Attribute to extract
546         * @return the modification item on the attributeType specified
547         */
548        public static final Modification getModificationItem( List<Modification> mods, AttributeType type )
549        {
550            for ( Modification modification:mods )
551            {
552                EntryAttribute attribute = modification.getAttribute();
553                
554                if ( attribute.getAttributeType() == type )
555                {
556                    return modification;
557                }
558            }
559            
560            return null;
561        }
562        
563        
564        /**
565         * Utility method to extract an attribute from a list of modifications.
566         * 
567         * @param mods the list of ModificationItems to extract the Attribute from.
568         * @param type the attributeType spec of the Attribute to extract
569         * @return the extract Attribute or null if no such attribute exists
570         */
571        public static EntryAttribute getAttribute( List<Modification> mods, AttributeType type )
572        {
573            Modification mod = getModificationItem( mods, type );
574            
575            if ( mod != null )
576            {
577                return mod.getAttribute();
578            }
579            
580            return null;
581        }
582        
583        
584        /**
585         * Encapsulate a ServerSearchResult enumeration into a SearchResult enumeration
586         * @param result The ServerSearchResult enumeration
587         * @return A SearchResultEnumeration
588         */
589        public static NamingEnumeration<SearchResult> toSearchResultEnum( final NamingEnumeration<ServerSearchResult> result )
590        {
591            if ( result instanceof EmptyEnumeration<?> )
592            {
593                return new EmptyEnumeration<SearchResult>();
594            }
595            
596            return new NamingEnumeration<SearchResult> ()
597            {
598                public void close() throws NamingException
599                {
600                    result.close();
601                }
602    
603    
604                /**
605                 * @see javax.naming.NamingEnumeration#hasMore()
606                 */
607                public boolean hasMore() throws NamingException
608                {
609                    return result.hasMore();
610                }
611    
612    
613                /**
614                 * @see javax.naming.NamingEnumeration#next()
615                 */
616                public SearchResult next() throws NamingException
617                {
618                    ServerSearchResult rec = result.next();
619                    
620                    SearchResult searchResult = new SearchResult( 
621                            rec.getDn().getName(), 
622                            rec.getObject(), 
623                            toBasicAttributes( rec.getServerEntry() ), 
624                            rec.isRelative() );
625                    
626                    return searchResult;
627                }
628                
629                
630                /**
631                 * @see java.util.Enumeration#hasMoreElements()
632                 */
633                public boolean hasMoreElements()
634                {
635                    return result.hasMoreElements();
636                }
637    
638    
639                /**
640                 * @see java.util.Enumeration#nextElement()
641                 */
642                public SearchResult nextElement()
643                {
644                    try
645                    {
646                        ServerSearchResult rec = result.next();
647        
648                        SearchResult searchResult = new SearchResult( 
649                                rec.getDn().getName(), 
650                                rec.getObject(), 
651                                toBasicAttributes( rec.getServerEntry() ), 
652                                rec.isRelative() );
653                        
654                        return searchResult;
655                    }
656                    catch ( NamingException ne )
657                    {
658                        NoSuchElementException nsee = 
659                            new NoSuchElementException( I18n.err( I18n.ERR_468 ) );
660                        nsee.initCause( ne );
661                        throw nsee;
662                    }
663                }
664            };
665        }
666        
667        
668        /**
669         * Remove the options from the attributeType, and returns the ID.
670         * 
671         * RFC 4512 :
672         * attributedescription = attributetype options
673         * attributetype = oid
674         * options = *( SEMI option )
675         * option = 1*keychar
676         */
677        private static String stripOptions( String attributeId )
678        {
679            int optionsPos = attributeId.indexOf( ";" ); 
680            
681            if ( optionsPos != -1 )
682            {
683                return attributeId.substring( 0, optionsPos );
684            }
685            else
686            {
687                return attributeId;
688            }
689        }
690        
691    
692        /**
693         * Get the options from the attributeType.
694         * 
695         * For instance, given :
696         * jpegphoto;binary;lang=jp
697         * 
698         * your get back a set containing { "binary", "lang=jp" }
699         */
700        private static Set<String> getOptions( String attributeId )
701        {
702            int optionsPos = attributeId.indexOf( ";" ); 
703    
704            if ( optionsPos != -1 )
705            {
706                Set<String> options = new HashSet<String>();
707                
708                String[] res = attributeId.substring( optionsPos + 1 ).split( ";" );
709                
710                for ( String option:res )
711                {
712                    if ( !StringTools.isEmpty( option ) )
713                    {
714                        options.add( option );
715                    }
716                }
717                
718                return options;
719            }
720            else
721            {
722                return null;
723            }
724        }
725    }