001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.servermanager;
029
030import java.util.Collections;
031import java.util.List;
032import java.util.Map;
033
034import edu.wisc.ssec.mcidasv.util.MakeToString;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 *
040 */
041public class LocalAddeEntry implements AddeEntry {
042
043    /** Friendly neighborhood logging object. */
044    static final Logger logger = LoggerFactory.getLogger(LocalAddeEntry.class);
045
046    /** Represents a {@literal "bad"} local ADDE entry. */
047    // seriously, don't use null unless you REALLY need it.
048    public static final LocalAddeEntry INVALID_ENTRY = new Builder("INVALID", "INVALID", "/dev/null", AddeFormat.INVALID).build();
049
050    /** Represents a {@literal "bad"} collection of local ADDE entries. */
051    public static final List<LocalAddeEntry> INVALID_ENTRIES = Collections.singletonList(INVALID_ENTRY);
052
053    /** Status of this entry. */
054    private EntryStatus entryStatus = EntryStatus.INVALID;
055
056    // RESOLV.SRV FIELDS
057    /** N1 */
058    private final String group;
059
060    /** N2 */
061    // this value is built in a non-obvious way. plz to be dox.
062    private final String descriptor;
063
064    /** RT */
065    private final boolean realtime;
066
067    /** MCV */
068    private final AddeFormat format;
069
070    /** R1 */
071    private final String start;
072
073    /** R2 */
074    private final String end;
075
076    /** MASK */
077    private final String fileMask;
078
079    /** C */
080    private final String name;
081    // END RESOLV.SRV FIELDS
082
083    private String asStringId;
084
085    /** Whether or not this entry is temporary. */
086    private final boolean isTemporary;
087
088    /** Allows the user to refer to this entry with an arbitrary name. */
089    private String entryAlias;
090
091    public enum ServerName {
092        // note: if you are adding a new server you may need to edit the
093        // AddeFormat enum below, the "formats" field in both
094        // LocalEntryEditor and LocalEntryShortcut, and the _formats dictionary
095        // in mcvadde.py.
096        ABIN, AREA, AMSE, AMSR, AMRR, GINI, FSDX, OMTP, LV1B, MODS, MODX, MOD4,  
097        MOD8, MODR, MSGS, MSGT, MTST, SCMI, SMIN, TMIN, MD, INDS, INDI, WARC, WARI, VIIR, INVALID
098    }
099
100    /**
101     * The various kinds of local ADDE data understood by McIDAS-V, along with
102     * some helpful metadata.
103     * 
104     * <ul>
105     * <li>{@literal "Human readable"} format names ({@link #friendlyName}).</li>
106     * <li>Optional tooltip description ({@link #tooltip}).</li>
107     * <li>Type of data ({@link #type}).</li>
108     * <li>File naming pattern {@link #fileFilter}.</li>
109     * </ul>
110     * 
111     * <p>None of {@code AddeFormat}'s fields should contain {@code null}.</p>
112     */
113    public enum AddeFormat {
114        // note: if you are adding a new value to this list, you may need to
115        // edit the ServerName enum, the "formats" field in both
116        // LocalEntryEditor and LocalEntryShortcut, and the _formats dictionary
117        // in mcvadde.py.
118        // sorry. :(
119        MCIDAS_AREA(ServerName.AREA, "McIDAS AREA"),
120        MCIDAS_MD(ServerName.MD, "McIDAS MD", "McIDAS MD", EntryType.POINT),
121        AMSRE_L1B(ServerName.AMSR, "AMSR-E L 1b", "AMSR-E Level 1b"),
122        AMSRE_L2A(ServerName.AMSE, "AMSR-E L 2a", "AMSR-E Level 2a"),
123        AMSRE_RAIN_PRODUCT(ServerName.AMRR, "AMSR-E Rain Product"),
124        GINI(ServerName.GINI, "GINI"),
125        GOES16_ABI(ServerName.ABIN, "GOES ABI", "GOES ABI"),
126        HIMAWARI8(ServerName.WARI, "Himawari 8", "Himawari 8"),
127        HIMAWARICAST(ServerName.WARC, "HimawariCast", "HimawariCast"),
128        INSAT3D_IMAGER(ServerName.INDI, "INSAT-3D Imager", "INSAT-3D Imager"),
129        INSAT3D_SOUNDER(ServerName.INDS, "INSAT-3D Sounder", "INSAT-3D Sounder"),
130        LRIT_GOES9(ServerName.FSDX, "LRIT GOES-9", "EUMETCast LRIT GOES-9"),
131        LRIT_GOES10(ServerName.FSDX, "LRIT GOES-10", "EUMETCast LRIT GOES-10"),
132        LRIT_GOES11(ServerName.FSDX, "LRIT GOES-11", "EUMETCast LRIT GOES-11"),
133        LRIT_GOES12(ServerName.FSDX, "LRIT GOES-12", "EUMETCast LRIT GOES-12"),
134        LRIT_MET5(ServerName.FSDX, "LRIT MET-5", "EUMETCast LRIT MET-5"),
135        LRIT_MET7(ServerName.FSDX, "LRIT MET-7", "EUMETCast LRIT MET-7"),
136        LRIT_MTSAT1R(ServerName.FSDX, "LRIT MTSAT-1R", "EUMETCast LRIT MTSAT-1R"),
137        METEOSAT_OPENMTP(ServerName.OMTP, "Meteosat OpenMTP"),
138        METOP_AVHRR_L1B(ServerName.LV1B, "Metop AVHRR L 1b", "Metop AVHRR Level 1b"),
139        MODIS_L1B_MOD02(ServerName.MODS, "MODIS MOD 02 - Level-1B Calibrated Geolocated Radiances", "MODIS Level 1b"),
140        MODIS_L2_MOD06(ServerName.MODX, "MODIS MOD 06 - Cloud Product", "MODIS Level 2 (Cloud Top Properties)"),
141        MODIS_L2_MOD07(ServerName.MODX, "MODIS MOD 07 - Atmospheric Profiles", "MODIS Level 2 (Atmospheric Profile)"),
142        MODIS_L2_MOD35(ServerName.MODX, "MODIS MOD 35 - Cloud Mask", "MODIS Level 2 (Cloud Mask)"),
143        MODIS_L2_MOD04(ServerName.MOD4, "MODIS MOD 04 - Aerosol Product", "MODIS Level 2 (Aerosol)"),
144        MODIS_L2_MOD28(ServerName.MOD8, "MODIS MOD 28 - Sea Surface Temperature", "MODIS Level 2 (Sea Surface Temperature)"),
145        MODIS_L2_MODR(ServerName.MODR, "MODIS MOD R - Corrected Reflectance", "MODIS Level 2 (Corrected Reflectance)"),
146        MSG_HRIT_FD(ServerName.MSGT, "MSG HRIT FD", "MSG HRIT (Full Disk)"),
147        MSG_HRIT_HRV(ServerName.MSGT, "MSG HRIT HRV", "MSG HRIT (High Resolution Visible)"),
148        MSG_NATIVE(ServerName.MSGS, "MSG Native Format", "MSG Native Format (*.nat) data"),
149        MTSAT_HRIT(ServerName.MTST, "MTSAT HRIT"),
150        NOAA_AVHRR_L1B(ServerName.LV1B, "NOAA AVHRR L 1b", "NOAA AVHRR Level 1b"),
151        SCMI(ServerName.SCMI, "SCMI", "Sectorized CMI"),
152        SSMI(ServerName.SMIN, "SSMI", "Terrascan netCDF (SMIN)"),
153        TRMM(ServerName.TMIN, "TRMM", "Terrascan netCDF (TMIN)"),
154        VIIRSD(ServerName.VIIR, "VIIRS SDR Day/Night Band", "JPSS VIIRS SDR Day/Night Band"),
155                VIIRSI(ServerName.VIIR, "VIIRS SDR I-Band", "JPSS VIIRS SDR I-Band"),
156        VIIRSM(ServerName.VIIR, "VIIRS SDR M-Band", "JPSS VIIRS SDR M-Band"),
157                VIIREI(ServerName.VIIR, "VIIRS EDR I-Band", "JPSS VIIRS EDR I-Band"),
158                VIIREM(ServerName.VIIR, "VIIRS EDR M-Band", "JPSS VIIRS EDR M-Band"),
159        INVALID(ServerName.INVALID, "", "", EntryType.INVALID);
160
161        /** Name of the McIDAS-X server. */
162        private final ServerName servName;
163
164        /** {@literal "Human readable"} format name. This is returned by {@link #toString()}. */
165        private final String friendlyName;
166
167        /** Description of the format. */
168        private final String tooltip;
169
170        /** Data type. Corresponds to {@code TYPE} in {@literal "RESOLV.SRV"}. */
171        private final EntryType type;
172
173        /** 
174         * Filename pattern used when listing files in a directory. 
175         * If {@link #servName} is {@link ServerName#MSGT} then 
176         * {@literal "*PRO*"} is used, otherwise {@literal "*"}. 
177         */
178        private final String fileFilter;
179
180        /**
181         * Builds an {@literal "ADDE format"} and its associated metadata in 
182         * a typesafe way.
183         * 
184         * @param servName {@link ServerName} that McIDAS-X uses for this format. 
185         * @param friendlyName {@literal "Human readable"} name of the format; returned by {@link #toString()}.
186         * @param tooltip If non-empty, this is used as a tooltip in the local entry editor.
187         * @param type {@link EntryType} used by this format.
188         */
189        AddeFormat(final ServerName servName, final String friendlyName, final String tooltip, final EntryType type) {
190            this.servName = servName;
191            this.friendlyName = friendlyName;
192            this.tooltip = tooltip;
193            this.type = type;
194            this.fileFilter = (servName != ServerName.MSGT) ? "*" : "*PRO*";
195        }
196
197        /**
198         * Builds an {@literal "imagery ADDE Format"} <b>without</b> a tooltip.
199         *
200         * @param servName {@link ServerName} that McIDAS-X uses for this format.
201         * @param friendlyName {@literal "Human readable"} name of the format; returned by {@link #toString()}.
202         */
203        AddeFormat(final ServerName servName, final String friendlyName) {
204            this(servName, friendlyName, "", EntryType.IMAGE);
205        }
206
207        /**
208         * Builds an {@literal "imagery ADDE Format"} <b>with</b> a tooltip.
209         *
210         * @param servName {@link ServerName} that McIDAS-X uses for this format.
211         * @param friendlyName {@literal "Human readable"} name of the format; returned by {@link #toString()}.
212         * @param tooltip If non-empty, this is used as a tooltip in the local entry editor.
213         */
214        AddeFormat(final ServerName servName, final String friendlyName, final String tooltip) {
215            this(servName, friendlyName, tooltip, EntryType.IMAGE);
216        }
217
218        /**
219         * Gets the McIDAS-X {@link ServerName} for this format.
220         *
221         * @return Either the name of this format's McIDAS-X server, or
222         * {@link ServerName#INVALID}.
223         */
224        public ServerName getServerName() {
225            return servName;
226        }
227
228        /**
229         * Gets the tooltip text to use in the server manager GUI for this
230         * format.
231         *
232         * @return Text to use as a GUI tooltip. Cannot be {@code null}, though
233         * empty {@code String} values are permitted.
234         */
235        public String getTooltip() {
236            return tooltip;
237        }
238
239        /**
240         * Gets the type of data used by this format. This value dictates the
241         * chooser(s) where this format can appear.
242         *
243         * @return One of {@link AddeEntry.EntryType EntryType}, or
244         * {@link AddeEntry.EntryType#INVALID INVALID}.
245         */
246        public EntryType getType() {
247            return type;
248        }
249
250        /**
251         * Gets the string used to filter out files that match this format.
252         *
253         * @return Either a specialized {@code String}, like {@literal "*PRO*"}
254         * or {@literal "*"}.
255         */
256        public String getFileFilter() {
257            return fileFilter;
258        }
259
260        /**
261         * Gets the {@code String} representation of this format.
262         *
263         * @return the value of {@link #friendlyName}.
264         */
265        @Override public String toString() {
266            return friendlyName;
267        }
268    }
269
270    /**
271     * Creates a new local ADDE entry from the given {@code builder} object.
272     *
273     * @param builder Builder that represents a local ADDE entry.
274     * 
275     * @see LocalAddeEntry.Builder
276     */
277    private LocalAddeEntry(final Builder builder) {
278        this.group = builder.group;
279        this.descriptor = builder.descriptor;
280        this.realtime = builder.realtime;
281        this.format = builder.format;
282        this.fileMask = builder.mask;
283        this.name = builder.name;
284        this.start = builder.start;
285        this.end = builder.end;
286        this.entryStatus = builder.status;
287        this.isTemporary = builder.temporary;
288        this.entryAlias = builder.alias;
289        logger.debug("created local: {}", this);
290    }
291
292    @Override public AddeAccount getAccount() {
293        return RemoteAddeEntry.DEFAULT_ACCOUNT;
294    }
295
296    @Override public String getAddress() {
297        return "localhost";
298    }
299
300    @Override public EntrySource getEntrySource() {
301        return EntrySource.USER;
302    }
303
304    @Override public EntryStatus getEntryStatus() {
305        return entryStatus;
306    }
307
308    @Override public String getEntryText() {
309        return "localhost/"+getGroup();
310    }
311
312    @Override public EntryType getEntryType() {
313        return format.getType();
314    }
315
316    @Override public EntryValidity getEntryValidity() {
317        return (isValid()) ? EntryValidity.VERIFIED : EntryValidity.INVALID;
318    }
319
320    // TODO(jon): fix this noop
321    @Override public String getEntryAlias() {
322        String tmp = entryAlias;
323        if (entryAlias == null) {
324            tmp = "";
325        }
326        return tmp;
327    }
328
329    // TODO(jon): fix this noop
330    @Override public void setEntryAlias(final String newAlias) {
331        if (newAlias == null) {
332            throw new NullPointerException("Null aliases are not allowable.");
333        }
334        this.entryAlias = newAlias;
335    }
336
337    @Override public void setEntryStatus(EntryStatus newStatus) {
338        entryStatus = newStatus;
339    }
340
341    @Override public boolean isEntryTemporary() {
342        return isTemporary;
343    }
344
345    @Override public String getGroup() {
346        return group;
347    }
348
349    @Override public String getName() {
350        return name;
351    }
352
353    /**
354     * Gets the ADDE descriptor for the current local ADDE entry.
355     * 
356     * @return ADDE descriptor (corresponds to the {@literal "N2"} section of
357     * a RESOLV.SRV entry).
358     */
359    public String getDescriptor() {
360        return descriptor;
361    }
362
363    /**
364     * Gets the ADDE dataset format for the current local ADDE entry.
365     * 
366     * @return ADDE format (corresponds to the {@literal "MCV"} section of a
367     * RESOLV.SRV entry).
368     */
369    public AddeFormat getFormat() {
370        return format;
371    }
372
373    /**
374     * Gets the ADDE file mask for the current local ADDE entry.
375     * 
376     * @return ADDE file mask (corresponds to the {@literal "MASK"} section
377     * of a RESOLV.SRV entry).
378     */
379    public String getMask() {
380        return fileMask;
381    }
382
383    /**
384     * Gets the ADDE file mask for the current local ADDE entry.
385     * 
386     * @return ADDE file mask (corresponds to the {@literal "MASK"} section
387     * of a RESOLV.SRV entry).
388     */
389    public String getFileMask() {
390        return fileMask;
391    }
392
393    /**
394     * Gets the ADDE realtime status of the current local ADDE entry.
395     * 
396     * @return Whether or not the current dataset is {@literal "realtime"}.
397     * Corresponds to the {@literal "RT"} section of a RESOLV.SRV entry.
398     */
399    public boolean getRealtime() {
400        return realtime;
401    }
402
403    /**
404     * Gets the starting number of the current local ADDE dataset.
405     * 
406     * @return Corresponds to the {@literal "R1"} section of a RESOLV.SRV entry.
407     */
408    public String getStart() {
409        return start;
410    }
411
412    /**
413     * Gets the ending number of the current local ADDE dataset.
414     * 
415     * @return Corresponds to the {@literal "R2"} section of a RESOLV.SRV entry.
416     */
417    public String getEnd() {
418        return end;
419    }
420
421    /**
422     * Tests the current local ADDE dataset for validity.
423     * 
424     * @return {@code true} iff {@link #group} and {@link #name} are not empty.
425     */
426    public boolean isValid() {
427//        return !((group.isEmpty()) || (descriptor.isEmpty()) || (name.isEmpty()));
428        return !(group.isEmpty() || name.isEmpty());
429    }
430
431    /**
432     * Gets the local ADDE dataset's realtime status as a value suitable for
433     * RESOLV.SRV (one of {@literal "Y"} or {@literal "N"}).
434     * 
435     * @return RESOLV.SRV-friendly representation of the current realtime status.
436     */
437    public String getRealtimeAsString() {
438        return realtime ? "Y" : "N";
439    }
440
441    /**
442     * @see LocalAddeEntry#generateHashCode(String, String, String, String, boolean, AddeFormat)
443     */
444    @Override public int hashCode() {
445        return generateHashCode(name, group, fileMask, entryAlias, isTemporary, format);
446    }
447
448    /**
449     * Checks a given object for equality with the current {@code LocalAddeEntry}
450     * instance.
451     * 
452     * @param obj Object to check. {@code null} values allowed.
453     * 
454     * @return {@code true} if {@code obj} is {@literal "equal"} to the current
455     * {@code LocalAddeEntry} instance.
456     */
457    @Override public boolean equals(Object obj) {
458        if (this == obj) {
459            return true;
460        }
461        if (obj == null) {
462            return false;
463        }
464        if (!(obj instanceof LocalAddeEntry)) {
465            return false;
466        }
467        LocalAddeEntry other = (LocalAddeEntry) obj;
468        if (fileMask == null) {
469            if (other.fileMask != null) {
470                return false;
471            }
472        } else if (!fileMask.equals(other.fileMask)) {
473            return false;
474        }
475        if (format == null) {
476            if (other.format != null) {
477                return false;
478            }
479        } else if (!format.toString().equals(other.format.toString())) {
480            return false;
481        }
482        if (group == null) {
483            if (other.group != null) {
484                return false;
485            }
486        } else if (!group.equals(other.group)) {
487            return false;
488        }
489        if (name == null) {
490            if (other.name != null) {
491                return false;
492            }
493        } else if (!name.equals(other.name)) {
494            return false;
495        }
496        if (entryAlias == null) {
497            if (other.entryAlias != null) {
498                return false;
499            }
500        } else if (!entryAlias.equals(other.entryAlias)) {
501            return false;
502        }
503        if (isTemporary != other.isTemporary) {
504            return false;
505        }
506        return true;
507    }
508
509    @Override public String asStringId() {
510        if (asStringId == null) {
511            asStringId = "localhost!"+group+'!'+EntryType.IMAGE.name()+'!'+name;
512        }
513        return asStringId;
514    }
515
516    @Override public String toString() {
517        return MakeToString.fromInstance(this)
518                           .add("name", name)
519                           .addQuoted("group", group)
520                           .addQuoted("fileMask", fileMask)
521                           .add("descriptor", descriptor)
522                           .add("serverName", format.getServerName().name())
523                           .add("format", format.name())
524                           .add("description", format.getTooltip())
525                           .add("type", format.getType())
526                           .add("status", entryStatus.name())
527                           .add("temporary", isTemporary)
528                           .add("alias", entryAlias).toString();
529    }
530
531    public static int generateHashCode(final LocalAddeEntry entry) {
532        return generateHashCode(entry.getName(), entry.getGroup(), entry.getMask(), entry.getEntryAlias(), entry.isEntryTemporary(), entry.getFormat());
533    }
534
535    public static int generateHashCode(String name, String group, String fileMask, String entryAlias, boolean isTemporary, AddeFormat format) {
536        final int prime = 31;
537        int result = 1;
538        result = prime * result
539            + ((fileMask == null) ? 0 : fileMask.hashCode());
540        result = prime * result + ((format == null) ? 0 : format.toString().hashCode());
541        result = prime * result + ((group == null) ? 0 : group.hashCode());
542        result = prime * result + ((name == null) ? 0 : name.hashCode());
543        result = prime * result + ((entryAlias == null) ? 0 : entryAlias.hashCode());
544        result = prime * result + (isTemporary ? 1231 : 1237);
545        return result;
546    }
547
548    /**
549     * A builder of (mostly) immutable {@link LocalAddeEntry} instances.
550     * 
551     * <p>Usage example: <pre>    {@code
552     *     LocalAddeEntry entry = new LocalAddeEntry
553     *         .Builder(group, name, format, mask)
554     *         .realtime("Y")
555     *         .range(start, end)
556     *         .type(EntryType.POINT)
557     *         .build();}</pre>
558     * 
559     * Only the values required by the Builder constructor are required.
560     */
561    public static class Builder {
562        // required
563        /** Corresponds to RESOLV.SRV's {@literal "N1"} section. */
564        private final String group;
565
566        /** Corresponds to RESOLV.SRV's {@literal "C"} section. */
567        private final String name;
568
569        /** Corresponds to RESOLV.SRV's {@literal "MCV"} section. */
570        private final AddeFormat format;
571
572        /** Corresponds to RESOLV.SRV's {@literal "MASK"} section. */
573        private final String mask;
574
575        // generated
576        private String descriptor;
577
578        // optional
579        /**
580         * Corresponds to RESOLV.SRV's {@literal "RT"} section.
581         * Defaults to {@code false}.
582         */
583        private boolean realtime = false;
584
585        /**
586         * Corresponds to RESOLV.SRV's {@literal "R1"} section.
587         * Defaults to {@literal "1"}.
588         */
589        private String start = "1";
590
591        /**
592         * Corresponds to RESOLV.SRV's {@literal "R2"} section.
593         * Defaults to {@literal "999999"}.
594         */
595        private String end = "999999";
596
597        /**
598         * Defaults to {@link AddeEntry.EntryStatus#INVALID}.
599         */
600        private EntryStatus status = EntryStatus.INVALID;
601
602        /**
603         * Corresponds to RESOLV.SRV's {@literal "TYPE"} section.
604         * Defaults to {@link AddeEntry.EntryType#IMAGE IMAGE}.
605         */
606        private EntryType type = EntryType.IMAGE;
607
608        /**
609         * Corresponds to RESOLV.SRV's {@literal "K"} section.
610         * Defaults to {@literal "NOT_SET"}.
611         */
612        private String kind = "NOT_SET";
613
614        /**
615         * Defaults to {@link ServerName#INVALID}.
616         */
617        private ServerName safeKind = ServerName.INVALID;
618
619        /** */
620        private boolean temporary = false;
621
622        /** */
623        private String alias = "";
624
625        public Builder(final Map<String, String> map) {
626            if (!map.containsKey("C") || !map.containsKey("N1") || !map.containsKey("MASK") || !map.containsKey("MCV")) {
627                throw new IllegalArgumentException("Cannot build a LocalAddeEntry without the following keys: C, N1, MASK, and MCV.");
628            }
629
630            this.name = map.get("C");
631            this.group = map.get("N1");
632            this.mask = map.get("MASK");
633            this.format = EntryTransforms.strToAddeFormat(map.get("MCV"));
634
635//            descriptor(map.get("N2"));
636            type(EntryTransforms.strToEntryType(map.get("TYPE")));
637            kind(map.get("K").toUpperCase());
638            realtime(map.get("RT"));
639            start(map.get("R1"));
640            end(map.get("R2"));
641            
642            if (map.containsKey("TEMPORARY")) {
643                temporary(map.get("TEMPORARY"));
644            }
645        }
646
647        /**
648         * Creates a new {@code LocalAddeEntry} {@literal "builder"} with the 
649         * required fields for a {@code LocalAddeEntry} object.
650         * 
651         * @param name Name of the local ADDE dataset.
652         * @param group ADDE group name.
653         * @param mask Local file mask.
654         * @param format Type of data.
655         */
656        public Builder(final String name, final String group, final String mask, final AddeFormat format) {
657            this.name = name;
658            this.group = group;
659            this.mask = mask;
660            this.format = format;
661        }
662
663        /**
664         * This method is currently a no-op.
665         *
666         * @param descriptor Local ADDE entry descriptor. Currently ignored.
667         *
668         * @return {@code LocalAddeEntry.Builder} with ADDE descriptor.
669         */
670        public Builder descriptor(final String descriptor) {
671//            if (descriptor != null) {
672//                this.descriptor = descriptor;
673//            }
674            return this;
675        }
676
677        /**
678         *
679         *
680         * @param realtimeAsStr Whether or not the local ADDE entry is
681         *                      {@literal "real time"}.
682         *
683         * @return {@code LocalAddeEntry.Builder} with ADDE realtime flag.
684         */
685        // looks like mcidasx understands ("Y"/"N"/"A")
686        // should probably ignore case and accept "YES"/"NO"/"ARCHIVE"
687        // in addition to the normal boolean conversion from String
688        public Builder realtime(final String realtimeAsStr) {
689            if (realtimeAsStr == null) {
690                return this;
691            }
692
693            if ("Y".equalsIgnoreCase(realtimeAsStr) || "YES".equalsIgnoreCase(realtimeAsStr)) {
694                this.realtime = true;
695            } else {
696                this.realtime = Boolean.valueOf(realtimeAsStr);
697            }
698            return this;
699        }
700
701        /**
702         *
703         *
704         * @param realtime Whether or not the local ADDE entry is
705         *                 {@literal "real time"}.
706         *
707         * @return {@code LocalAddeEntry.Builder} with ADDE realtime flag.
708         */
709        public Builder realtime(final boolean realtime) {
710            this.realtime = realtime;
711            return this;
712        }
713
714        /**
715         *
716         *
717         * @param type ADDE data type.
718         *
719         * @return {@code LocalAddeEntry.Builder} with ADDE data type.
720         */
721        // my assumption is that if "format" is known, you can infer "type"
722        public Builder type(final EntryType type) {
723            if (type != null) {
724                this.type = type;
725            }
726            return this;
727        }
728
729        /**
730         *
731         *
732         * @param kind ADDE server binary used to handle data.
733         *
734         * @return {@code LocalAddeEntry.Builder} with ADDE kind.
735         */
736        // my assumption is that if "format" is known, you can infer "kind"
737        public Builder kind(final String kind) {
738            if (kind == null) {
739                return this;
740            }
741
742            this.kind = kind;
743            try {
744                this.safeKind = ServerName.valueOf(kind);
745            } catch (IllegalArgumentException e) { 
746                this.safeKind = ServerName.INVALID;
747            }
748            return this;
749        }
750
751        /**
752         *
753         *
754         * @param start Beginning of local ADDE dataset.
755         *
756         * @return {@code LocalAddeEntry.Builder} with ADDE dataset
757         * {@literal "start"}.
758         */
759        public Builder start(final String start) {
760            if (start != null) {
761                this.start = start;
762            }
763            return this;
764        }
765
766        /**
767         *
768         *
769         * @param end End of local ADDE dataset.
770         *
771         * @return {@code LocalAddeEntry.Builder} with ADDE dataset
772         * {@literal "end"}.
773         */
774        public Builder end(final String end) {
775            if (end != null) {
776                this.end = end;
777            }
778            return this;
779        }
780
781        /**
782         *
783         *
784         * @param start Beginning of local ADDE dataset.
785         * @param end End of local ADDE dataset.
786         *
787         * @return {@code LocalAddeEntry.Builder} with ADDE dataset
788         * {@literal "start" and "end"} values.
789         */
790        public Builder range(final String start, final String end) {
791            if (start != null && end != null) {
792                this.start = start;
793                this.end = end;
794            }
795            return this;
796        }
797
798        /**
799         *
800         *
801         * @param status String representation of local ADDE entry status.
802         *
803         * @return {@code LocalAddeEntry.Builder} with
804         * {@link AddeEntry.EntryStatus}.
805         */
806        public Builder status(final String status) {
807            if (status != null && status.length() > 0) {
808                this.status = EntryTransforms.strToEntryStatus(status);
809            }
810            return this;
811        }
812
813        /**
814         * 
815         * 
816         * @param status Local ADDE entry status.
817         * 
818         * @return {@code LocalAddeEntry.Builder} with
819         * {@link AddeEntry.EntryStatus}.
820         */
821        public Builder status(final EntryStatus status) {
822            if (status != null) {
823                this.status = status;
824            }
825            return this;
826        }
827
828        /**
829         * 
830         * 
831         * @param temporary Whether or not the local ADDE entry will be saved
832         *                  between application sessions.
833         * 
834         * @return {@code LocalAddeEntry.Builder} with the specified temporary
835         * status.
836         */
837        public Builder temporary(final boolean temporary) {
838            this.temporary = temporary;
839            return this;
840        }
841
842        public Builder temporary(final String temporary) {
843            this.temporary = Boolean.valueOf(temporary);
844            return this;
845        }
846
847        /**
848         * 
849         * 
850         * @param alias Set an alias to use for the local ADDE entry.
851         * 
852         * @return {@code LocalAddeEntry.Builder} with the specified alias.
853         */
854        public Builder alias(final String alias) {
855            this.alias = alias;
856            return this;
857        }
858
859        /**
860         * 
861         * 
862         * @return New {@code LocalAddeEntry} instance.
863         */
864        public LocalAddeEntry build() {
865            // apparently need to hack up the descriptor for certain formats
866            switch (format) {
867                case MSG_HRIT_FD: this.descriptor = "FD"; break;
868                case MSG_HRIT_HRV: this.descriptor = "HRV"; break;
869                case LRIT_GOES9: this.descriptor = "GOES9"; break;
870                case LRIT_GOES10: this.descriptor = "GOES10"; break;
871                case LRIT_GOES11: this.descriptor = "GOES11"; break;
872                case LRIT_GOES12: this.descriptor = "GOES12"; break;
873                case LRIT_MET5: this.descriptor = "MET5"; break;
874                case LRIT_MET7: this.descriptor = "MET7"; break;
875                case LRIT_MTSAT1R: this.descriptor = "MTSAT1R"; break;
876                default:
877                    this.descriptor = Integer.toHexString(generateHashCode(name, group, mask, alias, temporary, format));
878                    break;
879            }
880            return new LocalAddeEntry(this);
881        }
882    }
883}