001/*
002 * Copyright 2007-2018 The jdeb developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.vafer.jdeb.debian;
018
019import java.io.BufferedReader;
020import java.io.ByteArrayInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.text.ParseException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033/**
034 * A control file as specified by the <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html">Debian policy</a>.
035 */
036public abstract class ControlFile {
037
038    protected final Map<String, String> values = new LinkedHashMap<String, String>();
039    protected final Map<String, String> userDefinedFields = new LinkedHashMap<String, String>();
040    protected final Set<ControlField> userDefinedFieldNames = new HashSet<ControlField>();
041
042    public void parse(String input) throws IOException, ParseException {
043        parse(new ByteArrayInputStream(input.getBytes("UTF-8")));
044    }
045
046    public void parse(InputStream input) throws IOException, ParseException {
047        BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
048        StringBuilder buffer = new StringBuilder();
049        String field = null;
050        int linenr = 0;
051        while (true) {
052            final String line = reader.readLine();
053
054            if (line == null) {
055                // flush value of the previous field
056                set(field, buffer.toString());
057                break;
058            }
059
060            linenr++;
061
062            if (line.length() == 0) {
063                throw new ParseException("Empty line", linenr);
064            }
065
066            final char first = line.charAt(0);
067            if (first == '#') {
068                // ignore commented out lines
069                continue;
070            }
071
072            if (Character.isLetter(first)) {
073
074                // new field
075
076                // flush value of the previous field
077                set(field, buffer.toString());
078                buffer = new StringBuilder();
079
080
081                final int i = line.indexOf(':');
082
083                if (i < 0) {
084                    throw new ParseException("Line misses ':' delimiter", linenr);
085                }
086
087                field = line.substring(0, i);
088                buffer.append(line.substring(i + 1).trim());
089
090                continue;
091            }
092
093            // continuing old value, lines with only a dot are ignored
094            buffer.append('\n');
095            if (!".".equals(line.substring(1).trim())) {
096                buffer.append(line.substring(1));
097            }
098        }
099        reader.close();
100
101    }
102
103    public void set(String field, final String value) {
104        if (field != null && isUserDefinedField(field)) {
105            userDefinedFields.put(field, value);
106            String fieldName = getUserDefinedFieldName(field);
107
108            if (fieldName != null) {
109                userDefinedFieldNames.add(new ControlField(fieldName));
110            }
111
112            field = fieldName;
113        }
114
115        if (field != null && !"".equals(field)) {
116            values.put(field, value);
117        }
118    }
119
120    public String get(String field) {
121        return values.get(field);
122    }
123
124    protected abstract ControlField[] getFields();
125
126    protected Map<String, String> getUserDefinedFields() {
127        return userDefinedFields;
128    }
129
130    protected Set<ControlField> getUserDefinedFieldNames() {
131        return userDefinedFieldNames;
132    }
133
134    public List<String> getMandatoryFields() {
135        List<String> fields = new ArrayList<String>();
136
137        for (ControlField field : getFields()) {
138            if (field.isMandatory()) {
139                fields.add(field.getName());
140            }
141        }
142
143        return fields;
144    }
145
146    public boolean isValid() {
147        return invalidFields().size() == 0;
148    }
149
150    public Set<String> invalidFields() {
151        Set<String> invalid = new HashSet<String>();
152
153        for (ControlField field : getFields()) {
154            if (field.isMandatory() && get(field.getName()) == null) {
155                invalid.add(field.getName());
156            }
157        }
158
159        return invalid;
160    }
161
162    public String toString(ControlField... fields) {
163        StringBuilder s = new StringBuilder();
164        for (ControlField field : fields) {
165            String value = values.get(field.getName());
166            s.append(field.format(value));
167        }
168        return s.toString();
169    }
170
171    public String toString() {
172        List<ControlField> fields = new ArrayList<ControlField>();
173        fields.addAll(Arrays.asList(getFields()));
174        fields.addAll(getUserDefinedFieldNames());
175        return toString(fields.toArray(new ControlField[fields.size()]));
176    }
177
178    /**
179     * Returns the letter expected in the prefix of a user defined field
180     * in order to include the field in this control file.
181     *
182     * @return The letter returned is:
183     * <ul>
184     *   <li>B: for a binary package</li>
185     *   <li>S: for a source package</li>
186     *   <li>C: for a changes file</li>
187     * </ul>
188     *
189     * @since 1.1
190     * @see <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html#s5.7">Debian Policy - User-defined fields</a>
191     */
192    protected abstract char getUserDefinedFieldLetter();
193
194    /**
195     * Tells if the specified field name is a user defined field.
196     * User-defined fields must begin with an 'X', followed by one or more
197     * letters that specify the output file and a hyphen.
198     *
199     * @param field the name of the field
200     *
201     * @since 1.1
202     * @see <a href="http://www.debian.org/doc/debian-policy/ch-controlfields.html#s5.7">Debian Policy - User-defined fields</a>
203     */
204    protected boolean isUserDefinedField(String field) {
205        return field.startsWith("X") && field.indexOf("-") > 0;
206    }
207
208    /**
209     * Returns the user defined field without its prefix.
210     *
211     * @param field the name of the user defined field
212     * @return the user defined field without the prefix, or null if the fields
213     *         doesn't apply to this control file.
214     * @since 1.1
215     */
216    protected String getUserDefinedFieldName(String field) {
217        int index = field.indexOf('-');
218        char letter = getUserDefinedFieldLetter();
219
220        for (int i = 0; i < index; ++i) {
221            if (field.charAt(i) == letter) {
222                return field.substring(index + 1);
223            }
224        }
225
226        return null;
227    }
228}