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 */
020package org.apache.directory.api.ldap.model.schema.syntaxCheckers;
021
022
023import java.util.HashSet;
024import java.util.Set;
025import java.util.regex.Pattern;
026import java.util.regex.PatternSyntaxException;
027
028import org.apache.directory.api.i18n.I18n;
029import org.apache.directory.api.ldap.model.constants.SchemaConstants;
030import org.apache.directory.api.ldap.model.schema.SyntaxChecker;
031import org.apache.directory.api.util.Strings;
032
033
034/**
035 * A SyntaxChecker which verifies that a value is a facsimile TelephoneNumber according 
036 * to ITU recommendation E.123 for the Telephone number part, and from RFC 4517, par. 
037 * 3.3.11 :
038 * 
039 * <pre>
040 * fax-number       = telephone-number *( DOLLAR fax-parameter )
041 * telephone-number = PrintableString
042 * fax-parameter    = "twoDimensional" |
043 *                    "fineResolution" |
044 *                    "unlimitedLength" |
045 *                    "b4Length" |
046 *                    "a3Width" |
047 *                    "b4Width" |
048 *                    "uncompressed"
049 * </pre>
050 * 
051 * If needed, and to allow more syntaxes, a list of regexps has been added
052 * which can be initialized to other values
053 * 
054 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
055 */
056@SuppressWarnings("serial")
057public final class FacsimileTelephoneNumberSyntaxChecker extends SyntaxChecker
058{
059    /** The default pattern used to check a TelephoneNumber */
060    private static final String DEFAULT_REGEXP = "^ *[+]? *((\\([0-9- ,;/#*]+\\))|[0-9- ,;/#*]+)+$";
061    
062    /** The default pattern */
063    private final String defaultRegexp;
064
065    /** The compiled default pattern */
066    private Pattern defaultPattern;
067    
068    /** Fax parameters possible values */
069    private static final String TWO_DIMENSIONAL = "twoDimensional";
070    private static final String FINE_RESOLUTION = "fineResolution";
071    private static final String UNLIMITED_LENGTH = "unlimitedLength";
072    private static final String B4_LENGTH = "b4Length";
073    private static final String A3_LENGTH = "a3Width";
074    private static final String B4_WIDTH = "b4Width";
075    private static final String UNCOMPRESSED = "uncompressed";
076
077    /** A set which contains all the possible fax parameters values */
078    private static Set<String> faxParameters = new HashSet<>();
079
080    /** Initialization of the fax parameters set of values */
081    static
082    {
083        faxParameters.add( Strings.toLowerCaseAscii( TWO_DIMENSIONAL ) );
084        faxParameters.add( Strings.toLowerCaseAscii( FINE_RESOLUTION ) );
085        faxParameters.add( Strings.toLowerCaseAscii( UNLIMITED_LENGTH ) );
086        faxParameters.add( Strings.toLowerCaseAscii( B4_LENGTH ) );
087        faxParameters.add( Strings.toLowerCaseAscii( A3_LENGTH ) );
088        faxParameters.add( Strings.toLowerCaseAscii( B4_WIDTH ) );
089        faxParameters.add( Strings.toLowerCaseAscii( UNCOMPRESSED ) );
090    }
091    
092    /**
093     * A static instance of FacsimileTelephoneNumberSyntaxChecker
094     */
095    public static final FacsimileTelephoneNumberSyntaxChecker INSTANCE = 
096        new FacsimileTelephoneNumberSyntaxChecker( SchemaConstants.FACSIMILE_TELEPHONE_NUMBER_SYNTAX );
097    
098    /**
099     * A static Builder for this class
100     */
101    public static final class Builder extends SCBuilder<FacsimileTelephoneNumberSyntaxChecker>
102    {
103        /** The compiled default pattern */
104        private String defaultRegexp;
105
106        /** The compiled default pattern */
107        private Pattern defaultPattern;
108
109        /**
110         * The Builder constructor
111         */
112        private Builder()
113        {
114            super( SchemaConstants.FACSIMILE_TELEPHONE_NUMBER_SYNTAX );
115            setDefaultRegexp( DEFAULT_REGEXP );
116        }
117
118
119        /**
120         * Create a new instance of FacsimileTelephoneNumberSyntaxChecker
121         * @return A new instance of FacsimileTelephoneNumberSyntaxChecker
122         */
123        @Override
124        public FacsimileTelephoneNumberSyntaxChecker build()
125        {
126            return new FacsimileTelephoneNumberSyntaxChecker( oid, defaultRegexp, defaultPattern );
127        }
128
129
130        /**
131         * Set the default regular expression for the Telephone number
132         * 
133         * @param regexp the default regular expression.
134         */
135        public Builder setDefaultRegexp( String regexp )
136        {
137            defaultRegexp = regexp;
138            
139            try
140            {
141                defaultPattern = Pattern.compile( regexp );
142            }
143            catch ( PatternSyntaxException pse )
144            {
145                // Roll back to the default pattern
146                defaultPattern = Pattern.compile( DEFAULT_REGEXP );
147            }
148
149            return this;
150        }
151    }
152
153
154    /**
155     * Creates a new instance of TelephoneNumberSyntaxChecker.
156     */
157    private FacsimileTelephoneNumberSyntaxChecker( String oid )
158    {
159        this( oid, DEFAULT_REGEXP, Pattern.compile( DEFAULT_REGEXP ) );
160    }
161
162
163    /**
164     * Creates a new instance of TelephoneNumberSyntaxChecker.
165     */
166    private FacsimileTelephoneNumberSyntaxChecker( String oid, String defaultRegexp, Pattern defaultPattern )
167    {
168        super( oid );
169
170        this.defaultPattern = defaultPattern;
171        this.defaultRegexp = defaultRegexp;
172    }
173
174
175    /**
176     * @return An instance of the Builder for this class
177     */
178    public static Builder builder()
179    {
180        return new Builder();
181    }
182
183
184    /**
185     * Get the default regexp (either the original one, or the one that has been set)
186     * 
187     * @return The default regexp
188     */
189    public String getRegexp()
190    {
191        if ( defaultRegexp == null )
192        {
193            return DEFAULT_REGEXP;
194        }
195        else
196        {
197            return defaultRegexp;
198        }
199    }
200
201
202    /**
203     * {@inheritDoc}
204     */
205    @Override
206    public boolean isValidSyntax( Object value )
207    {
208        String strValue;
209
210        if ( value == null )
211        {
212            if ( LOG.isDebugEnabled() )
213            {
214                LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, "null" ) );
215            }
216            
217            return false;
218        }
219
220        if ( value instanceof String )
221        {
222            strValue = ( String ) value;
223        }
224        else if ( value instanceof byte[] )
225        {
226            strValue = Strings.utf8ToString( ( byte[] ) value );
227        }
228        else
229        {
230            strValue = value.toString();
231        }
232
233        if ( strValue.length() == 0 )
234        {
235            if ( LOG.isDebugEnabled() )
236            {
237                LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
238            }
239            
240            return false;
241        }
242
243        // The facsimile telephone number might be composed
244        // of two parts separated by a '$'.
245        int dollarPos = strValue.indexOf( '$' );
246
247        if ( dollarPos == -1 )
248        {
249            // We have no fax-parameter : check the Telephone number
250            boolean result = defaultPattern.matcher( strValue ).matches();
251
252            if ( LOG.isDebugEnabled() )
253            {
254                if ( result )
255                {
256                    LOG.debug( I18n.msg( I18n.MSG_04489_SYNTAX_VALID, value ) );
257                }
258                else
259                {
260                    LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
261                }
262            }
263
264            return result;
265        }
266
267        // First check the telephone number if the '$' is not at the first position
268        if ( dollarPos > 0 )
269        {
270            boolean result = defaultPattern.matcher( strValue.substring( 0, dollarPos - 1 ) ).matches();
271
272            if ( LOG.isDebugEnabled() )
273            {
274                if ( result )
275                {
276                    LOG.debug( I18n.msg( I18n.MSG_04489_SYNTAX_VALID, value ) );
277                }
278                else
279                {
280                    LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
281                    
282                    return false;
283                }
284            }
285
286            // Now, try to validate the fax-parameters : we may
287            // have more than one, so we will store the seen params
288            // in a set to check that we don't have the same param twice
289            Set<String> paramsSeen = new HashSet<>();
290
291            while ( dollarPos > 0 )
292            {
293                String faxParam;
294                int newDollar = strValue.indexOf( '$', dollarPos + 1 );
295
296                if ( newDollar == -1 )
297                {
298                    faxParam = strValue.substring( dollarPos + 1 );
299                }
300                else
301                {
302                    faxParam = strValue.substring( dollarPos + 1, newDollar );
303                }
304
305                if ( faxParam.length() == 0 )
306                {
307                    // Not allowed
308                    if ( LOG.isDebugEnabled() )
309                    {
310                        LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
311                    }
312                    
313                    return false;
314                }
315
316                // Relax a little bit the syntax by lowercasing the param
317                faxParam = Strings.toLowerCaseAscii( faxParam );
318
319                if ( !faxParameters.contains( faxParam ) || paramsSeen.contains( faxParam ) )
320                {
321                    // This parameter is not in the possible set
322                    if ( LOG.isDebugEnabled() )
323                    {
324                        LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
325                    }
326                    
327                    return false;
328                }
329                else
330                {
331                    // It's a correct param, let's add it to the seen 
332                    // params.
333                    paramsSeen.add( faxParam );
334                }
335
336                dollarPos = newDollar;
337            }
338
339            if ( LOG.isDebugEnabled() )
340            {
341                LOG.debug( I18n.msg( I18n.MSG_04489_SYNTAX_VALID, value ) );
342            }
343            
344            return true;
345        }
346
347        // We must have a valid telephone number !
348        if ( LOG.isDebugEnabled() )
349        {
350            LOG.debug( I18n.err( I18n.ERR_04488_SYNTAX_INVALID, value ) );
351        }
352        
353        return false;
354    }
355}