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.mina.integration.beans;
021
022import java.beans.PropertyEditor;
023import java.util.Collection;
024import java.util.LinkedHashMap;
025import java.util.Map;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029/**
030 * A {@link PropertyEditor} which converts a {@link String} into
031 * a {@link Collection} and vice versa.
032 *
033 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
034 */
035public class MapEditor extends AbstractPropertyEditor {
036    static final Pattern ELEMENT = Pattern.compile("([,\\s]+)|"
037            + // Entry delimiter
038            "(\\s*=\\s*)|"
039            + // Key-Value delimiter
040            "(?<=\")((?:\\\\\"|\\\\'|\\\\\\\\|\\\\ |[^\"])*)(?=\")|"
041            + "(?<=')((?:\\\\\"|\\\\'|\\\\\\\\|\\\\ |[^'])*)(?=')|" + "((?:[^\\\\\\s'\",]|\\\\ |\\\\\"|\\\\')+)");
042
043    private final Class<?> keyType;
044
045    private final Class<?> valueType;
046
047    public MapEditor(Class<?> keyType, Class<?> valueType) {
048        if (keyType == null) {
049            throw new IllegalArgumentException("keyType");
050        }
051        if (valueType == null) {
052            throw new IllegalArgumentException("valueType");
053        }
054        this.keyType = keyType;
055        this.valueType = valueType;
056        getKeyEditor();
057        getValueEditor();
058        setTrimText(false);
059    }
060
061    private PropertyEditor getKeyEditor() {
062        PropertyEditor e = PropertyEditorFactory.getInstance(keyType);
063        if (e == null) {
064            throw new IllegalArgumentException("No key " + PropertyEditor.class.getSimpleName() + " found for "
065                    + keyType.getSimpleName() + '.');
066        }
067        return e;
068    }
069
070    private PropertyEditor getValueEditor() {
071        PropertyEditor e = PropertyEditorFactory.getInstance(valueType);
072        if (e == null) {
073            throw new IllegalArgumentException("No value " + PropertyEditor.class.getSimpleName() + " found for "
074                    + valueType.getSimpleName() + '.');
075        }
076        return e;
077    }
078
079    @Override
080    protected final String toText(Object value) {
081        StringBuilder buf = new StringBuilder();
082        
083        for (Object o : ((Map<?,?>) value).entrySet()) {
084            Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
085            Object ekey = entry.getKey();
086            Object evalue = entry.getValue();
087
088            PropertyEditor ekeyEditor = PropertyEditorFactory.getInstance(ekey);
089            if (ekeyEditor == null) {
090                throw new IllegalArgumentException("No key " + PropertyEditor.class.getSimpleName() + " found for "
091                        + ekey.getClass().getSimpleName() + '.');
092            }
093            ekeyEditor.setValue(ekey);
094
095            PropertyEditor evalueEditor = PropertyEditorFactory.getInstance(evalue);
096            if (evalueEditor == null) {
097                throw new IllegalArgumentException("No value " + PropertyEditor.class.getSimpleName() + " found for "
098                        + evalue.getClass().getSimpleName() + '.');
099            }
100            ekeyEditor.setValue(ekey);
101            evalueEditor.setValue(evalue);
102
103            // TODO normalize.
104            String keyString = ekeyEditor.getAsText();
105            String valueString = evalueEditor.getAsText();
106            buf.append(keyString);
107            buf.append(" = ");
108            buf.append(valueString);
109            buf.append(", ");
110        }
111
112        // Remove the last delimiter.
113        if (buf.length() >= 2) {
114            buf.setLength(buf.length() - 2);
115        }
116        return buf.toString();
117    }
118
119    @Override
120    protected final Object toValue(String text) throws IllegalArgumentException {
121        PropertyEditor keyEditor = getKeyEditor();
122        PropertyEditor valueEditor = getValueEditor();
123        Map<Object, Object> answer = newMap();
124        Matcher m = ELEMENT.matcher(text);
125        TokenType lastTokenType = TokenType.ENTRY_DELIM;
126        Object key = null;
127        Object value = null;
128
129        while (m.find()) {
130            if (m.group(1) != null) {
131                switch (lastTokenType) {
132                case VALUE:
133                case ENTRY_DELIM:
134                    break;
135                default:
136                    throw new IllegalArgumentException("Unexpected entry delimiter: " + text);
137                }
138
139                lastTokenType = TokenType.ENTRY_DELIM;
140                continue;
141            }
142
143            if (m.group(2) != null) {
144                if (lastTokenType != TokenType.KEY) {
145                    throw new IllegalArgumentException("Unexpected key-value delimiter: " + text);
146                }
147
148                lastTokenType = TokenType.KEY_VALUE_DELIM;
149                continue;
150            }
151
152            // TODO escape here.
153            String region = m.group();
154
155            if (m.group(3) != null || m.group(4) != null) {
156                // Skip the last '"'.
157                m.region(m.end() + 1, m.regionEnd());
158            }
159
160            switch (lastTokenType) {
161            case ENTRY_DELIM:
162                keyEditor.setAsText(region);
163                key = keyEditor.getValue();
164                lastTokenType = TokenType.KEY;
165                break;
166            case KEY_VALUE_DELIM:
167                valueEditor.setAsText(region);
168                value = valueEditor.getValue();
169                lastTokenType = TokenType.VALUE;
170                answer.put(key, value);
171                break;
172            case KEY:
173            case VALUE:
174                throw new IllegalArgumentException("Unexpected key or value: " + text);
175            }
176        }
177
178        return answer;
179    }
180
181    protected Map<Object, Object> newMap() {
182        return new LinkedHashMap<Object, Object>();
183    }
184
185    private static enum TokenType {
186        ENTRY_DELIM, KEY_VALUE_DELIM, KEY, VALUE,
187    }
188}