Changeset 3011 for OAIHarvester


Ignore:
Timestamp:
06/10/13 19:50:23 (11 years ago)
Author:
oschonef
Message:
  • update Woodstox, SLF4J and Apache Commons HTTP client dependencies
  • several interoperability fixes (including relaxed behavior)
  • several bug fixes
Location:
OAIHarvester/trunk/OAIHarvester
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • OAIHarvester/trunk/OAIHarvester/pom.xml

    r1905 r3011  
    6161        <groupId>org.apache.httpcomponents</groupId>
    6262        <artifactId>httpclient</artifactId>
    63         <version>4.1.3</version>
     63        <version>4.2.5</version>
    6464    </dependency>
    6565
     
    6767        <groupId>org.codehaus.woodstox</groupId>
    6868        <artifactId>woodstox-core-lgpl</artifactId>
    69         <version>4.1.3</version>
     69        <version>4.2.0</version>
    7070    </dependency>
    7171
     
    8585    <dependency>
    8686      <groupId>org.slf4j</groupId>
    87       <artifactId>slf4j-jdk14</artifactId>
     87      <artifactId>slf4j-log4j12</artifactId>
    8888      <version>${slf4j.version}</version>
    8989      <scope>test</scope>
     
    9898    <maven.compiler.target>1.6</maven.compiler.target>
    9999    <!-- versions of common dependencies -->
    100     <slf4j.version>1.6.4</slf4j.version>
     100    <slf4j.version>1.7.2</slf4j.version>
    101101  </properties>
    102102
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/ProtocolError.java

    r1128 r3011  
    1515        public static Code fromString(String s) {
    1616            if ("cannotDisseminateFormat".equals(s)) {
    17                return CANNOT_DISSERMINATE_FORMAT;
     17                return CANNOT_DISSERMINATE_FORMAT;
    1818            } else if ("idDoesNotExist".equals(s)) {
    1919                return ID_DOES_NOR_EXIST;
     
    3434            }
    3535        }
    36     }
     36    } // enum Code
    3737
    3838    private final Code code;
    3939    private final String message;
    40    
     40
     41
    4142    public ProtocolError(String code, String message) {
    42         this.code = Code.fromString(code);
     43        this.code    = Code.fromString(code);
    4344        this.message = message;
    4445    }
     46
    4547
    4648    public Code getCode() {
    4749        return code;
    4850    }
    49    
     51
     52
    5053    public String getMessage() {
    5154        return message;
    5255    }
     56
    5357
    5458    public String toString() {
     
    5660    }
    5761
     62    public boolean isCode(Code code) {
     63        if (code == null) {
     64            throw new NullPointerException("code == null");
     65        }
     66        return code.equals(this.code);
     67    }
     68
    5869} // class ProtocolError
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/AbstractHarvester.java

    r1903 r3011  
    77import java.util.List;
    88
     9import javax.xml.namespace.QName;
    910import javax.xml.stream.XMLInputFactory;
    1011import javax.xml.stream.XMLStreamException;
    11 import javax.xml.stream.XMLStreamReader;
    1212
    1313import org.codehaus.stax2.XMLInputFactory2;
     
    2222import eu.clarin.cmdi.oai.harvester.ext.DescriptionParser;
    2323
     24
    2425abstract class AbstractHarvester implements Harvester {
    2526    private static final int DEFAULT_MAX_NETWORK_RETRY_COUNT = 5;
    2627    protected int maxNetworkRetryCount = DEFAULT_MAX_NETWORK_RETRY_COUNT;
    27     private final XMLInputFactory factory;
    28 //    private final XMLValidationSchema schema;
     28    private final XMLInputFactory2 factory;
     29    // private final XMLValidationSchema schema;
    2930    private List<DescriptionParser> descriptionParsers =
    30         new ArrayList<DescriptionParser>();
     31            new ArrayList<DescriptionParser>();
     32
    3133
    3234    protected AbstractHarvester() {
    33         factory = XMLInputFactory2.newInstance();
     35        factory = (XMLInputFactory2) XMLInputFactory.newInstance();
    3436
    3537        // Stax settings
     
    5456    }
    5557
     58
     59    @Override
    5660    public final void registerDescriptionParser(DescriptionParser parser)
    5761            throws HarvesterException {
     
    6165        if (findDescriptionParser(parser.getNamespaceURI(),
    6266                parser.getLocalName()) != null) {
    63             throw new HarvesterException("description parser for '{" +
    64                     parser.getNamespaceURI() + "}" + parser.getLocalName() +
    65                     "' was already registered");
     67            throw new HarvesterException("description parser for '" +
     68                    new QName(parser.getNamespaceURI(), parser.getLocalName()) +
     69                            "' was already registered");
    6670        }
    6771        if (descriptionParsers == null) {
     
    7074        descriptionParsers.add(parser);
    7175    }
    72    
     76
     77
     78    @Override
    7379    public abstract HarvestJob createJob(URI repositoryURI,
    7480            HarvestHandler handler) throws HarvesterException;
    7581
     82
     83    @Override
    7684    public final HarvestJob createJob(URI repositoryURI)
    7785            throws HarvesterException {
    7886        return createJob(repositoryURI, null);
    7987    }
    80    
     88
     89
    8190    @Override
    8291    public final HarvestJob createJob(String repositoryURI,
     
    8998    }
    9099
     100
    91101    @Override
    92102    public final HarvestJob createJob(String repositoryURI)
     
    95105    }
    96106
     107
    97108    @Override
    98109    public final HarvestJob findJob(long id) throws HarvesterException {
    99110        return doFindJob(id);
    100111    }
     112
    101113
    102114    @Override
     
    109121    }
    110122
     123
    111124    @Override
    112125    public final int getMaxNetworkRetryCount() {
    113126        return maxNetworkRetryCount;
    114127    }
    115    
     128
     129
    116130    @Override
    117131    public final void setMaxNetworkRetryCount(int maxNetworkRetryCount) {
     
    123137        this.maxNetworkRetryCount = maxNetworkRetryCount;
    124138    }
     139
    125140
    126141    DescriptionParser findDescriptionParser(String namespaceURI,
     
    137152    }
    138153
     154
    139155    abstract void doRunJob(HarvestJobImpl job) throws HarvesterException;
     156
    140157
    141158    abstract void doCancelJob(HarvestJobImpl job) throws HarvesterException;
    142159
    143     protected abstract HarvestJob doFindJob(long id)
    144         throws HarvesterException;
    145160
    146     protected final XMLStreamReader createReader(InputStream in)
     161    protected abstract HarvestJob doFindJob(long id) throws HarvesterException;
     162
     163
     164    protected final XMLStreamReader2 createReader(InputStream in)
    147165            throws XMLStreamException {
    148166        XMLStreamReader2 reader =
    149             (XMLStreamReader2) factory.createXMLStreamReader(in);
     167                (XMLStreamReader2) factory.createXMLStreamReader(in);
    150168        // reader.validateAgainst(schema);
    151169        return reader;
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/HarvestJobImpl.java

    r1163 r3011  
    2323import eu.clarin.cmdi.oai.harvester.Repository.Granularity;
    2424
     25
    2526final class HarvestJobImpl implements HarvestJob, Delayed {
    2627    public enum Task {
    27         IDENTIFY_REPOSITORY,
    28         ENUMERATE_METADATA_FORMATS,
    29         HARVEST_RECORDS
    30     }
     28        IDENTIFY_REPOSITORY, ENUMERATE_METADATA_FORMATS, HARVEST_RECORDS
     29    }
     30
    3131    private static final class StatisticsImpl implements Statistics {
    3232        private final String prefix;
     
    3636        private long bytedTransferred = 0;
    3737        private Date latestDatestamp = null;
    38        
     38
     39
    3940        private StatisticsImpl(String prefix) {
    4041            this.prefix = prefix;
    4142        }
     43
    4244
    4345        @Override
     
    4648        }
    4749
     50
    4851        @Override
    4952        public long getRecordCount() {
     
    5154        }
    5255
     56
    5357        private void incRecordCount() {
    5458            this.recordCount++;
    5559        }
     60
    5661
    5762        @Override
     
    6065        }
    6166
     67
    6268        private void incRequestCount() {
    6369            this.requestCount++;
    6470        }
     71
    6572
    6673        @Override
     
    6976        }
    7077
     78
    7179        private void incResumptionCount() {
    7280            this.resumptionCount++;
    7381        }
     82
    7483
    7584        @Override
     
    7786            return bytedTransferred;
    7887        }
    79    
     88
     89
    8090        private void incBytesTransferred(long bytes) {
    8191            this.bytedTransferred += bytes;
    8292        }
     93
    8394
    8495        @Override
     
    8697            return latestDatestamp;
    8798        }
    88        
     99
     100
    89101        private void updateLatestDatestmp(Date latestDatestamp) {
    90102            if (this.latestDatestamp == null) {
     
    97109        }
    98110    } // inner class Statistics
     111
    99112    private final AbstractHarvester harvester;
    100113    private final long id;
     
    131144    private String resumptionToken = null;
    132145
     146
    133147    HarvestJobImpl(AbstractHarvester harvester, long id, URI repositoryURI,
    134148            HarvestHandler handler) {
    135         this.harvester = harvester;
    136         this.id = id;
     149        this.harvester     = harvester;
     150        this.id            = id;
    137151        this.repositoryURI = repositoryURI;
    138         this.handler = handler;
    139     }
     152        this.handler       = handler;
     153    }
     154
    140155
    141156    @Override
     
    144159    }
    145160
     161
    146162    @Override
    147163    public State getState() {
     
    149165    }
    150166
     167
    151168    @Override
    152169    public boolean isRunning() {
     
    154171    }
    155172
     173
    156174    @Override
    157175    public long getTotelRecordCount() {
     
    159177    }
    160178
     179
    161180    @Override
    162181    public int getTotelRequestCount() {
     
    164183    }
    165184
     185
    166186    @Override
    167187    public long getTotalBytesTransferred() {
     
    169189    }
    170190
     191
    171192    @Override
    172193    public long getTotalTime() {
     
    174195    }
    175196
     197
    176198    @Override
    177199    public long getNetworkTime() {
    178200        return timeNetwork;
    179201    }
    180    
     202
     203
    181204    @Override
    182205    public long getWaitTime() {
     
    184207    }
    185208
     209
    186210    @Override
    187211    public long getProcessingTime() {
     
    189213    }
    190214
    191    
     215
    192216    @Override
    193217    public HarvestHandler getHarvestHandler() {
     
    195219    }
    196220
     221
    197222    @Override
    198223    public Date getFrom() {
    199224        return from;
    200225    }
    201    
     226
     227
    202228    @Override
    203229    public void setFrom(Date from) {
     
    207233        this.from = from;
    208234    }
    209    
     235
     236
    210237    @Override
    211238    public Date getUntil() {
    212239        return until;
    213240    }
    214    
     241
     242
    215243    @Override
    216244    public void setUntil(Date until) {
     
    221249    }
    222250
     251
    223252    @Override
    224253    public String getSet() {
    225254        return set;
    226255    }
     256
    227257
    228258    @Override
     
    238268    }
    239269
     270
    240271    @Override
    241272    public List<String> getMetadataPrefixes() {
    242273        return metadataPrefixes;
    243274    }
    244    
     275
     276
    245277    @Override
    246278    public void setMetadataPrefixes(List<String> metadataPrefixes) {
     
    254286        }
    255287    }
    256    
     288
     289
    257290    @Override
    258291    public Statistics getStatistics(String prefix) {
     
    271304    }
    272305
     306
    273307    @Override
    274308    public List<Statistics> getStatistics() {
     
    283317    }
    284318
     319
    285320    @Override
    286321    public void run() throws HarvesterException {
    287322        harvester.doRunJob(this);
    288323    }
     324
    289325
    290326    @Override
     
    294330    }
    295331
     332
    296333    @Override
    297334    public long getDelay(TimeUnit unit) {
     
    299336                TimeUnit.MILLISECONDS);
    300337    }
     338
     339
     340    @Override
     341    public boolean equals(Object obj) {
     342        if (obj == null) {
     343            return false;
     344        }
     345        if (obj == this) {
     346            return true;
     347        }
     348        if (!(obj instanceof HarvestJobImpl)) {
     349            return false;
     350        }
     351        final HarvestJobImpl rhs = (HarvestJobImpl) obj;
     352        return (this.id == rhs.id) && this.harvester.equals(rhs.harvester);
     353    }
     354
     355    @Override
     356    public int hashCode() {
     357        return super.hashCode();
     358    }
     359   
    301360
    302361    @Override
     
    312371        }
    313372    }
     373
    314374
    315375    @Override
     
    327387    }
    328388
     389
    329390    public URI getRepositoryURI() {
    330391        return repositoryURI;
    331392    }
     393
    332394
    333395    void setState(State state) {
     
    341403    }
    342404
     405
    343406    void setRepositoryName(String repositoryName) {
    344407        this.repositoryName = repositoryName;
    345408    }
    346409
     410
    347411    void setBaseURL(String baseURL) {
    348412        this.baseURL = baseURL;
    349413    }
    350414
     415
    351416    void setProtocolVersion(String protocolVersion) {
    352417        this.protocolVersion = protocolVersion;
    353418    }
    354    
     419
     420
    355421    void setAdminEmail(List<String> adminEmail) {
    356422        this.adminEmail = adminEmail;
    357423    }
    358424
     425
    359426    void setEarliestTimestamp(Date earliestTimestamp) {
    360427        this.earliestTimestamp = earliestTimestamp;
    361428    }
    362429
     430
    363431    void setDeletedNotion(DeletedNotion deletedNotion) {
    364432        this.deletedNotion = deletedNotion;
    365433    }
    366434
     435
    367436    void setGranularity(Granularity granularity) {
    368437        this.granularity = granularity;
    369438    }
    370439
     440
    371441    Granularity getGranularity() {
    372442        return granularity;
    373443    }
    374444
     445
    375446    void setCompressionMask(int compressionMask) {
    376447        this.compressionMask = compressionMask;
    377448    }
    378    
     449
     450
    379451    int getCompressionMask() {
    380452        return compressionMask;
    381453    }
    382454
     455
    383456    void setDescriptions(List<Description> descriptions) {
    384457        this.descriptions = descriptions;
    385458    }
     459
    386460
    387461    void incRequestCount() {
     
    393467    }
    394468
     469
    395470    void incResumptionCount() {
    396471        final StatisticsImpl stats = getCurrentStatistics();
     
    400475    }
    401476
     477
    402478    void addToNetworkTime(long delta) {
    403479        timeNetwork += delta;
    404480    }
     481
    405482
    406483    void finishRequest(long bytesTransferred, long timeProcessingDelta) {
     
    413490    }
    414491
     492
    415493    boolean isState(State state) {
    416494        return this.state == state;
    417495    }
    418496
     497
    419498    Task getTask() {
    420499        return task;
    421500    }
    422501
     502
    423503    void setTask(Task task) {
    424504        this.task = task;
    425505    }
     506
    426507
    427508    String getCurrentPrefix() {
     
    441522    }
    442523
     524
    443525    boolean removeCurrentPrefix() {
    444526        prefixWorklist.remove(0);
     
    446528    }
    447529
     530
    448531    String getResumptionToken() {
    449532        return resumptionToken;
    450533    }
    451    
     534
     535
    452536    void setResumptionToken(String resumptionToken) {
    453537        this.resumptionToken = resumptionToken;
    454538    }
    455539
     540
    456541    boolean isHarvestingNewPrefix() {
    457542        return (resumptionToken == null) && (networkRetryCount == 0);
    458543    }
    459544
     545
    460546    int incNetworkRetryCount() {
    461547        return ++networkRetryCount;
    462548    }
    463    
     549
     550
    464551    int getNetworkRetryCount() {
    465552        return networkRetryCount;
    466553    }
     554
    467555
    468556    void resetNetworkRetryCount() {
     
    471559    }
    472560
     561
    473562    void setNetworkRequestDelay(long delay) {
    474563        this.timeDelayed += delay;
    475564        this.delayUntil = System.currentTimeMillis() + delay;
    476565    }
     566
    477567
    478568    void onStartListingRecords() {
     
    488578    }
    489579
     580
    490581    void onFinishListingRecords() {
    491582        Date latestDatestamp = null;
     
    501592        resumptionToken = null;
    502593    }
     594
    503595
    504596    void onRecord(Header header) {
     
    514606    }
    515607
     608
    516609    void onRecordMetadata(Header header, XMLStreamReader reader) {
    517610        if (handler != null) {
     
    520613    }
    521614
     615
    522616    void onRecordAbout(Header header, XMLStreamReader reader) {
    523617        if (handler != null) {
     
    526620    }
    527621
     622
    528623    void onIdentify() {
    529624        if (handler != null) {
     
    533628                    return repositoryName;
    534629                }
    535                
     630
     631
    536632                @Override
    537633                public String getProtocolVersion() {
    538634                    return protocolVersion;
    539635                }
    540                
     636
     637
    541638                @Override
    542639                public Granularity getGranularity() {
    543640                    return granularity;
    544641                }
    545                
     642
     643
    546644                @Override
    547645                public Date getEarliestTimestamp() {
    548646                    return earliestTimestamp;
    549647                }
    550                
     648
     649
    551650                @Override
    552651                public DeletedNotion getDeletedNotion() {
    553652                    return deletedNotion;
    554653                }
    555                
     654
     655
    556656                @Override
    557657                public int getCompressionMask() {
    558658                    return compressionMask;
    559659                }
    560                
     660
     661
    561662                @Override
    562663                public String getBaseURL() {
    563664                    return baseURL;
    564665                }
    565                
     666
     667
    566668                @Override
    567669                public List<String> getAdminEmail() {
    568                     return (adminEmail != null) ?
    569                             Collections.unmodifiableList(adminEmail) : null;
    570                 }
     670                    return (adminEmail != null) ? Collections
     671                            .unmodifiableList(adminEmail) : null;
     672                }
     673
    571674
    572675                @Override
    573676                public List<Description> getDescriptions() {
    574                     return (descriptions != null) ?
    575                             Collections.unmodifiableList(descriptions) : null;
     677                    return (descriptions != null) ? Collections
     678                            .unmodifiableList(descriptions) : null;
    576679                }
    577680            };
     
    579682        }
    580683    }
     684
    581685
    582686    void onListMetadataFormats(List<MetadataFormat> metadataFormats) {
     
    588692    }
    589693
     694
    590695    InputStream wrap(InputStream stream) throws IOException {
    591696        if (handler != null) {
     
    596701    }
    597702
     703
    598704    private StatisticsImpl getCurrentStatistics() {
    599705        if ((statistics != null) && !statistics.isEmpty()) {
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/HarvestWorker.java

    r1163 r3011  
    77import java.util.List;
    88
     9import javax.xml.namespace.QName;
    910import javax.xml.stream.XMLStreamException;
    1011
     
    1213import org.apache.http.HttpResponse;
    1314import org.apache.http.HttpStatus;
     15import org.apache.http.client.HttpClient;
    1416import org.apache.http.client.methods.HttpGet;
     17import org.apache.http.client.utils.HttpClientUtils;
    1518import org.apache.http.impl.client.DefaultHttpClient;
    1619import org.apache.http.impl.cookie.DateParseException;
     
    1922import org.apache.http.params.CoreProtocolPNames;
    2023import org.apache.http.params.HttpParams;
     24import org.apache.http.util.EntityUtils;
    2125import org.joda.time.DateTime;
    2226import org.joda.time.DateTimeZone;
     
    2428import org.joda.time.format.DateTimeFormat;
    2529import org.joda.time.format.DateTimeFormatter;
     30import org.slf4j.Logger;
     31import org.slf4j.LoggerFactory;
    2632
    2733import eu.clarin.cmdi.oai.harvester.Description;
    2834import eu.clarin.cmdi.oai.harvester.HarvestJob.State;
    2935import eu.clarin.cmdi.oai.harvester.HarvesterException;
     36import eu.clarin.cmdi.oai.harvester.HarvesterProtocolErrorException;
    3037import eu.clarin.cmdi.oai.harvester.MetadataFormat;
     38import eu.clarin.cmdi.oai.harvester.ProtocolError;
    3139import eu.clarin.cmdi.oai.harvester.Repository;
    3240import eu.clarin.cmdi.oai.harvester.Repository.DeletedNotion;
     
    3442import eu.clarin.cmdi.oai.harvester.ext.DescriptionParser;
    3543import eu.clarin.cmdi.oai.harvester.impl.HarvestJobImpl.Task;
     44
    3645
    3746public class HarvestWorker {
     
    4857    private static final String COMPRESSION_GZIP = "gzip";
    4958    private static final String VERB_IDENTIFY = "Identify";
    50     private static final String VERB_LIST_METADATAFORMATS =
    51         "ListMetadataFormats";
     59    private static final String VERB_LIST_METADATAFORMATS = "ListMetadataFormats";
    5260    private static final String VERB_LIST_RECORDS = "ListRecords";
    5361    private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
    5462    private static final String HEADER_CONNECTION = "Connection";
    55     private static final String[] DATEFORMATS_DAYS =
    56         { "yyyy'-'MM'-'dd" };
    57     private static final String[] DATEFORMATS_FULL =
    58         { "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", "yyyy'-'MM'-'dd" };
     63    private static final String DATEFORMAT_SECONDS = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'";
     64    private static final String DATEFORMAT_DAYS = "yyyy'-'MM'-'dd";
     65    private static final String[] DATEFORMATS =
     66        { DATEFORMAT_SECONDS, DATEFORMAT_DAYS };
     67    private static final Logger logger =
     68            LoggerFactory.getLogger(HarvestWorker.class);
    5969    private final AbstractHarvester harvester;
    60     private final DefaultHttpClient client;
     70    private final HttpClient client;
    6171    private final RequestUriBuilder uriBuilder;
    6272    private final HeaderImpl header;
    6373    private final XmlStreamReaderProxy readerWrapper;
    6474    private long delay;
     75
    6576
    6677    HarvestWorker(AbstractHarvester harvester) {
     
    6980        client = new DefaultHttpClient();
    7081        client.getParams().setParameter(CoreProtocolPNames.USER_AGENT,
    71                     "eu.clarin.cmdi.oai.Harvester/0.0.1");
     82                "eu.clarin.cmdi.oai.Harvester/0.0.1");
    7283        // request stuff
    7384        uriBuilder = new RequestUriBuilder();
    7485        // other stuff
    75         this.header = new HeaderImpl();
     86        this.header        = new HeaderImpl();
    7687        this.readerWrapper = new XmlStreamReaderProxy();
    7788    }
     89
    7890
    7991    long process(HarvestJobImpl job, int maxNetworkTries)
     
    115127                } else if ((job.getResumptionToken() != null) && (delay == 0)) {
    116128                    /*
    117                      * special case: request successful, but list was split
    118                      * die to OAI flow control
     129                     * special case: request successful, but list was split due
     130                     * to OAI flow control
    119131                     */
    120132                    ok = true;
     
    151163    }
    152164
     165
    153166    private boolean doIdentifyTask(HarvestJobImpl job) throws IOException,
    154167            XMLStreamException, HarvesterException {
     
    171184            s = response.readContent(OAI_NS, "protocolVersion", true);
    172185            if (!PROTOCOL_VERSION.equals(s)) {
    173                 throw new HarvesterException("unsupported protocol version: " +
    174                         s);
     186                throw new HarvesterException(
     187                        "unsupported protocol version: " + s);
    175188            }
    176189            job.setProtocolVersion(PROTOCOL_VERSION);
     
    178191            List<String> adminEmail = null;
    179192            do {
    180                 s = response.readContent(OAI_NS, "adminEmail",
    181                         (adminEmail == null));
     193                s = response.readContent(OAI_NS,
     194                        "adminEmail", (adminEmail == null));
    182195                if (s != null) {
    183196                    if (adminEmail == null) {
     
    190203
    191204            /*
    192              *  defer setting of earliestDatestamp in job until we know
    193              *  the datestamp granularity of the repository
     205             * defer setting of earliestDatestamp in job until we know the
     206             * datestamp granularity of the repository
    194207             */
    195208            final String earliestDatestamp =
    196                 response.readContent(OAI_NS, "earliestDatestamp", true);
     209                    response.readContent(OAI_NS, "earliestDatestamp", true);
    197210
    198211            s = response.readContent(OAI_NS, "deletedRecord", true);
     
    242255                        namespaceURI, localName);
    243256                if (parser != null) {
    244                     Description desc =
    245                         parser.parseDescription(response.getXMLStreamReader());
     257                    Description desc = parser.parseDescription(
     258                            response.getXMLStreamReader());
    246259                    if (desc != null) {
    247260                        if (descriptions == null) {
     
    251264                    }
    252265                } else {
    253                     System.err.println("skipping DESC " + namespaceURI + ", " +
    254                             localName);
     266                    logger.info("skipping <description> for {}", new QName(
     267                            namespaceURI, localName));
    255268                }
    256269                response.readEnd(OAI_NS, "description", true);
     
    259272            response.readEnd(OAI_NS, "Identify");
    260273            response.close();
    261            
     274
    262275            job.onIdentify();
    263276
     
    269282        }
    270283    }
     284
    271285
    272286    private boolean doListMetadataFormatsTask(HarvestJobImpl job)
     
    282296            response.readStart(OAI_NS, "ListMetadataFormats", true);
    283297            List<MetadataFormat> metadataFormats =
    284                 new ArrayList<MetadataFormat>();
     298                    new ArrayList<MetadataFormat>();
    285299            while (response.readStart(OAI_NS, "metadataFormat",
    286300                    metadataFormats.isEmpty())) {
    287301                final String prefix =
    288                     response.readContent(OAI_NS, "metadataPrefix", true);
     302                        response.readContent(OAI_NS, "metadataPrefix", true);
    289303                final String schema =
    290                     response.readContent(OAI_NS, "schema", true);
     304                        response.readContent(OAI_NS, "schema", true);
    291305                final String namespace =
    292                     response.readContent(OAI_NS, "metadataNamespace", true);
     306                        response.readContent(OAI_NS, "metadataNamespace", true);
    293307                metadataFormats.add(
    294308                        new MetadataFormatImpl(prefix, schema, namespace));
     
    299313
    300314            job.onListMetadataFormats(metadataFormats);
    301            
     315
    302316            return true;
    303317        } finally {
     
    307321        }
    308322    }
     323
    309324
    310325    private boolean doListRecordsTask(HarvestJobImpl job) throws IOException,
     
    344359            boolean first = true;
    345360            while (response.readStart(OAI_NS, "record", first)) {
     361                boolean deleted = false;
     362
    346363                response.readStart(OAI_NS, "header", true, true);
    347364                String s = response.readAttributeValue(null, "status");
     
    349366                    if ("deleted".equals(s)) {
    350367                        header.setDeleted(true);
     368                        deleted = true;
    351369                    } else {
    352                         throw new XMLStreamException("attribute 'status' of " +
    353                                 "element 'record' must contain 'deleted'");
     370                        throw new XMLStreamException("attribute 'status' of "
     371                                + "element 'record' must contain 'deleted'",
     372                                response.getLocation());
    354373                    }
    355374                }
     
    365384                response.readEnd(OAI_NS, "header");
    366385                job.onRecord(header);
    367                 if (response.readStart(OAI_NS, "metadata", true)) {
    368                     response.consumeWhitespace();
    369                     readerWrapper.reset(response.getXMLStreamReader());
    370                     job.onRecordMetadata(header, readerWrapper);
    371                     response.consumeWhitespace();
    372                     response.readEnd(OAI_NS, "metadata");
     386                /*
     387                 * only parse metadata from non-deleted records
     388                 */
     389                if (!deleted) {
     390                    if (response.readStart(OAI_NS, "metadata", true)) {
     391                        response.consumeWhitespace();
     392                        readerWrapper.reset(response.getXMLStreamReader());
     393                        job.onRecordMetadata(header, readerWrapper);
     394                        response.consumeWhitespace();
     395                        response.readEnd(OAI_NS, "metadata");
     396                    }
    373397                }
    374398                while (response.readStart(OAI_NS, "about", false)) {
     
    398422            }
    399423            response.readEnd(OAI_NS, "ListRecords");
    400            
     424
    401425            response.close();
    402426
    403427            return (nextToken == null);
     428        } catch (HarvesterProtocolErrorException e) {
     429            /*
     430             * XXX: handle NO_RECORDS_MATCH protocol error
     431             */
     432            List<ProtocolError> errors = e.getErrors();
     433            if ((errors != null) && !errors.isEmpty()) {
     434                for (ProtocolError error : errors) {
     435                    if (error.isCode(ProtocolError.Code.NO_RECORDS_MATCH)) {
     436                        logger.debug("no records match prefix '{}'",
     437                                job.getCurrentPrefix());
     438                        return true;
     439                    }
     440                }
     441            }
     442            throw e;
    404443        } finally {
    405444            if (response != null) {
     
    409448    }
    410449
     450
    411451    private Response execute(HarvestJobImpl job, boolean compress)
    412452            throws IOException, XMLStreamException, HarvesterException {
    413         long now = System.currentTimeMillis();
     453        final long now = System.currentTimeMillis();
    414454        job.incRequestCount();
    415455        final URI uri = uriBuilder.createURI(job.getRepositoryURI());
    416         System.err.println("HTTP GET " + uri);
     456        logger.debug("Performing HTTP GET request to {}", uri);
    417457        final HttpGet request = new HttpGet(uri);
    418458        if (compress) {
    419             if ((job.getCompressionMask() &
    420                     Repository.COMPRESSION_METHOD_GZIP) > 0) {
     459            final int mask = job.getCompressionMask();
     460            if ((mask & Repository.COMPRESSION_METHOD_GZIP) > 0) {
    421461                request.addHeader(HEADER_ACCEPT_ENCODING, "gzip");
    422462            }
    423             if ((job.getCompressionMask() &
    424                     Repository.COMPRESSION_METHOD_DEFLATE) > 0) {
     463            if ((mask & Repository.COMPRESSION_METHOD_DEFLATE) > 0) {
    425464                request.addHeader(HEADER_ACCEPT_ENCODING, "deflate");
    426465            }
     
    434473
    435474        final HttpResponse response = client.execute(request);
    436         job.addToNetworkTime(System.currentTimeMillis() - now);
    437         int status = response.getStatusLine().getStatusCode();
    438         if (status == HttpStatus.SC_OK) {
    439             delay = 0;
    440             return new Response(job, response.getEntity(), harvester);
    441         } else {
    442             request.abort();
    443             if (status == HttpStatus.SC_SERVICE_UNAVAILABLE) {
    444                 System.err.println("-> " + status);
    445                 delay = parseTryAfter(response);
     475        /*
     476         * FIXME: the following code need serious re-factoring
     477         */
     478        boolean close = false;
     479        try {
     480            job.addToNetworkTime(System.currentTimeMillis() - now);
     481            final int status = response.getStatusLine().getStatusCode();
     482            if (status == HttpStatus.SC_OK) {
     483                delay = 0;
     484                return new Response(job, response, harvester);
    446485            } else {
    447                 throw new HarvesterException(
    448                         "provider returned unexpected HTTP status: " + status);
    449             }
    450         }
    451         return null;
    452     }
     486                if (status == HttpStatus.SC_SERVICE_UNAVAILABLE) {
     487                    delay = parseTryAfter(response);
     488                    logger.debug("got service unavailable status, retrying " +
     489                            "after {} seconds", delay);
     490                    close = true;
     491                    return null;
     492                } else {
     493                    throw new HarvesterException(
     494                            "provider returned unexpected HTTP status: " +
     495                                    status);
     496                }
     497            }
     498        } catch (IOException e) {
     499            close = true;
     500            throw e;
     501        } catch (XMLStreamException e) {
     502            close = true;
     503            throw e;
     504        } catch (HarvesterException e) {
     505            close = true;
     506            throw e;
     507        } finally {
     508            if (close) {
     509                /*
     510                 * try hard to release HTTP client resources ...
     511                 */
     512                try {
     513                    EntityUtils.consume(response.getEntity());
     514                } catch (IOException ex) {
     515                    /* IGNORE */
     516                }
     517
     518                /* make sure to release allocated resources */
     519                HttpClientUtils.closeQuietly(response);
     520
     521                if (request != null) {
     522                    request.abort();
     523                }
     524            }
     525        }
     526    }
     527
    453528
    454529    private long parseTryAfter(HttpResponse response) {
     
    474549    }
    475550
    476     private Date parseDate(Repository.Granularity granularity, String s)
     551
     552    private static Date parseDate(Repository.Granularity granularity, String s)
    477553            throws HarvesterException {
    478         String[] patterns = null;
     554        final MutableDateTime date = new MutableDateTime(0);
     555        for (int i = 0; i < DATEFORMATS.length; i++) {
     556            DateTimeFormatter fmt = DateTimeFormat
     557                    .forPattern(DATEFORMATS[i])
     558                    .withZone(DateTimeZone.UTC);
     559            if (fmt.parseInto(date, s, 0) == s.length()) {
     560                if (DATEFORMATS[i].equals(DATEFORMAT_SECONDS) &&
     561                        (granularity == Repository.Granularity.DAYS)) {
     562                    logger.warn("repository announced DAYS granularity but " +
     563                            "provided timestamp with SECONDS granularity");
     564                }
     565                if (DATEFORMATS[i].equals(DATEFORMAT_DAYS)) {
     566                    date.setTime(0, 0, 0, 0);
     567                }
     568                return date.toDate();
     569            }
     570        }
     571        throw new HarvesterException("invalid date: " + s);
     572    }
     573
     574
     575    private static String formatDate(Repository.Granularity granularity,
     576            Date date) throws HarvesterException {
     577        DateTimeFormatter fmt = null;
    479578        switch (granularity) {
    480579        case DAYS:
    481             patterns = DATEFORMATS_DAYS;
     580            fmt = DateTimeFormat.forPattern(DATEFORMAT_DAYS)
     581                .withZone(DateTimeZone.UTC);
    482582            break;
    483583        case SECONDS:
    484             patterns = DATEFORMATS_FULL;
    485         }
    486         MutableDateTime date = new MutableDateTime(0);
    487         for (int i = 0; i < patterns.length; i++) {
    488             DateTimeFormatter fmt = DateTimeFormat
    489                 .forPattern(patterns[i])
     584            fmt = DateTimeFormat.forPattern(DATEFORMAT_SECONDS)
    490585                .withZone(DateTimeZone.UTC);
    491             if (fmt.parseInto(date, s, 0) == s.length()) {
    492                 if (patterns[i].equals(DATEFORMATS_DAYS[0])) {
    493                     date.setTime(0, 0, 0, 0);
    494                 }
    495                 return date.toDate();
    496             }
    497         }
    498         throw new HarvesterException("invalid date: " + s);
    499     }
    500 
    501     private String formatDate(Repository.Granularity granularity, Date date) {
    502         String pattern = null;
    503         switch (granularity) {
    504         case DAYS:
    505             pattern = DATEFORMATS_DAYS[0];
    506586            break;
    507         case SECONDS:
    508             pattern = DATEFORMATS_FULL[0];
    509         }
    510         DateTimeFormatter fmt =
    511             DateTimeFormat.forPattern(pattern).withZone(DateTimeZone.UTC);
     587        default:
     588            /* cannot happen, but silence FindBugs warning */
     589            throw new HarvesterException("invalid granularity: " + granularity);
     590        }
    512591        return fmt.print(new DateTime(date));
    513592    }
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/ParallelHarvester.java

    r1148 r3011  
    88import java.util.concurrent.atomic.AtomicLong;
    99
     10import org.slf4j.Logger;
     11import org.slf4j.LoggerFactory;
     12
    1013import eu.clarin.cmdi.oai.harvester.HarvestHandler;
    1114import eu.clarin.cmdi.oai.harvester.HarvestJob;
     
    1316import eu.clarin.cmdi.oai.harvester.HarvesterException;
    1417
     18
    1519public class ParallelHarvester extends AbstractHarvester {
    1620    private final class WorkerThread extends Thread {
    17         private final HarvestWorker worker =
    18             new HarvestWorker(ParallelHarvester.this);
     21        private final HarvestWorker worker = new HarvestWorker(
     22                ParallelHarvester.this);
    1923        private AtomicBoolean loop = new AtomicBoolean(true);
    20        
     24
     25
    2126        @Override
    2227        public void run() {
    2328            while (loop.get()) {
    2429                try {
    25                     System.err.println(getId() + " waiting ...");
    26                     HarvestJobImpl job = queue.take();
    27                     System.err.println(getId() + " got job " + job.getId());
     30                    logger.debug("[{}] waiting ...", getId());
     31                    final HarvestJobImpl job = queue.take();
     32                    logger.debug("[{}] got job {}", getId(), job.getId());
    2833                    try {
    2934                        while (true) {
    30                             System.err.println(getId() + " process job " +
    31                                     job.getId());
     35                            logger.debug("[{}] processing job {} ...",
     36                                    getId(), job.getId());
    3237                            worker.process(job, maxNetworkRetryCount);
    3338                            if (job.isRunning()) {
    3439                                if (job.getDelay(TimeUnit.MILLISECONDS) > 0) {
    3540                                    queue.offer(job);
    36                                     System.err.println(getId() +
    37                                             " requeuing job " + job.getId());
     41                                    logger.debug("[{}] requeuing job {} ...",
     42                                            getId(), job.getId());
    3843                                } else {
    3944                                    continue;
     
    4954                }
    5055            } // while
    51             System.err.println(getId() + " ... exit");
     56            logger.debug("[{}] exiting ...", getId());
    5257        }
     58
    5359
    5460        void shutdown() {
     
    5763        }
    5864    }
     65    private static final Logger logger =
     66            LoggerFactory.getLogger(ParallelHarvester.class);
     67    private final HashSet<WorkerThread> workers =
     68            new HashSet<WorkerThread>();
     69    private final DelayQueue<HarvestJobImpl> queue =
     70            new DelayQueue<HarvestJobImpl>();
     71    private final AtomicLong nextId = new AtomicLong(0);
    5972
    60     private final HashSet<WorkerThread> workers = new HashSet<WorkerThread>();
    61     private final DelayQueue<HarvestJobImpl> queue = new DelayQueue<HarvestJobImpl>();
    62     private AtomicLong nextId = new AtomicLong();
    6373
    6474    private ParallelHarvester() {
     
    6878    }
    6979
     80
    7081    @Override
    7182    public HarvestJob createJob(URI repositoryURI, HarvestHandler handler)
     
    7485                repositoryURI, handler);
    7586    }
     87
    7688
    7789    public static Harvester newInstance() {
     
    8395    }
    8496
     97
    8598    @Override
    8699    public void shutdown() {
     
    90103                w.join();
    91104            } catch (InterruptedException e) {
    92                 System.err.println("join fail!");
     105                logger.error("interrupted while shutting down");
    93106            }
    94107        }
     
    102115    }
    103116
     117
    104118    @Override
    105119    void doCancelJob(HarvestJobImpl job) throws HarvesterException {
    106120        queue.remove(job);
    107121    }
     122
    108123
    109124    @Override
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/Response.java

    r1904 r3011  
    1010import java.util.zip.InflaterInputStream;
    1111
     12import javax.xml.namespace.NamespaceContext;
    1213import javax.xml.namespace.QName;
     14import javax.xml.stream.Location;
    1315import javax.xml.stream.XMLStreamConstants;
    1416import javax.xml.stream.XMLStreamException;
    1517import javax.xml.stream.XMLStreamReader;
     18import javax.xml.stream.XMLStreamWriter;
    1619
    1720import org.apache.http.HttpEntity;
     21import org.apache.http.HttpResponse;
     22import org.apache.http.client.utils.HttpClientUtils;
    1823import org.apache.http.util.EntityUtils;
     24import org.codehaus.stax2.XMLStreamReader2;
     25import org.slf4j.Logger;
     26import org.slf4j.LoggerFactory;
    1927
    2028import eu.clarin.cmdi.oai.harvester.HarvesterException;
     
    2230import eu.clarin.cmdi.oai.harvester.ProtocolError;
    2331
     32
    2433final class Response {
    2534    private static final class CountingInputStream extends FilterInputStream {
    2635        private long count = 0;
    2736
     37
    2838        private CountingInputStream(final InputStream stream) {
    2939            super(stream);
    3040        }
     41
    3142
    3243        @Override
     
    3647            return result;
    3748        }
     49
    3850
    3951        @Override
     
    4557        }
    4658
     59
    4760        @Override
    4861        public int read(byte[] buffer) throws IOException {
     
    5265        }
    5366
     67
    5468        @Override
    5569        public long skip(long n) throws IOException {
     
    5872            return result;
    5973        }
    60        
     74
     75
    6176        public long getByteCount() {
    6277            return count;
     
    6479
    6580    } // class CountingInputStream
    66 
    67     private static final String OAI_NS =
    68         "http://www.openarchives.org/OAI/2.0/";
    69     private final HttpEntity entity;
     81    private static final String OAI_NS = "http://www.openarchives.org/OAI/2.0/";
     82    private static final String ENCODING_GZIP = "gzip";
     83    private static final String ENCODING_DEFLATE = "defalte";
     84    private static final Logger logger =
     85            LoggerFactory.getLogger(Response.class);
     86    private final HttpResponse response;
    7087    private final long now = System.currentTimeMillis();
    71     private final CountingInputStream stream;
    72     private final XMLStreamReader reader;
    73 
    74     Response(HarvestJobImpl job, HttpEntity entity, AbstractHarvester harvester)
     88    private final CountingInputStream stream;
     89    private final XMLStreamReader2 reader;
     90
     91
     92    Response(HarvestJobImpl job, HttpResponse response, AbstractHarvester harvester)
    7593            throws IOException, XMLStreamException, HarvesterException {
    76         this.entity = entity;
    77 
     94        if (job == null) {
     95           throw new NullPointerException("job == null");
     96        }
     97        if (response == null) {
     98            throw new NullPointerException("response == null");
     99        }
     100        if (harvester == null) {
     101            throw new NullPointerException("harvester == null");
     102        }
     103        this.response = response;
     104
     105        final HttpEntity entity = response.getEntity();
    78106        if (entity.getContentType() != null) {
    79             System.err.println("Content-Type: " +
    80                     entity.getContentType().getValue());
    81             System.err.println("Content-Length: " + entity.getContentLength());
    82         }
    83         InputStream in = entity.getContent();
     107            logger.debug("Content-Type: {}", entity.getContentType().getValue());
     108            logger.debug("Content-Length: {}", entity.getContentLength());
     109        }
     110        InputStream in             = entity.getContent();
    84111        org.apache.http.Header enc = entity.getContentEncoding();
    85112        if (enc != null) {
    86             System.err.println("Content-Encoding: " +
    87                     entity.getContentEncoding().getValue());
    88             if ("gzip".equalsIgnoreCase(enc.getValue())) {
     113            final String encoding = enc.getValue();
     114            logger.debug("Content-Encoding: {}", encoding);
     115            if (ENCODING_GZIP.equalsIgnoreCase(encoding)) {
    89116                in = new GZIPInputStream(in);
    90             } else if ("deflate".equalsIgnoreCase(enc.getValue())) {
     117            } else if (ENCODING_DEFLATE.equalsIgnoreCase(encoding)) {
    91118                in = new InflaterInputStream(in, new Inflater(true));
     119            } else {
     120                throw new HarvesterException("Unsupported content encoding " +
     121                        "in HTTP response: " + encoding);
    92122            }
    93123        }
     
    97127        readContent(OAI_NS, "responseDate", true);
    98128        readStart(OAI_NS, "request", true, true);
    99         // System.err.println("YYY XXX = " + toReadable(reader) + ", " +
    100         // reader.getAttributeValue(null, "verb"));
    101129        readEnd(OAI_NS, "request", true);
    102130
     
    116144        }
    117145    }
    118    
     146
     147
    119148    public void close() throws XMLStreamException {
    120149        readEnd(OAI_NS, "OAI-PMH");
    121150    }
     151
    122152
    123153    public void release(HarvestJobImpl job) {
     
    134164            /* IGNORE */
    135165        }
     166
     167        /*
     168         * try hard to release HTTP client resources ...
     169         */
    136170        try {
    137             EntityUtils.consume(entity);
    138         } catch (IOException e) {
     171            EntityUtils.consume(response.getEntity());
     172        } catch (IOException ex) {
    139173            /* IGNORE */
    140174        }
    141     }
    142 
    143     public boolean readStart(String namespaceURI, String localName,
    144             boolean required) throws XMLStreamException {
     175
     176        /* make sure to release allocated resources */
     177        HttpClientUtils.closeQuietly(response);
     178    }
     179
     180
     181    XMLStreamReader getXMLStreamReader() {
     182        return reader;
     183    }
     184
     185
     186    Location getLocation() {
     187        return reader.getLocation();
     188    }
     189
     190
     191    boolean readStart(String namespaceURI, String localName, boolean required)
     192            throws XMLStreamException {
    145193        return readStart(namespaceURI, localName, required, false);
    146194    }
    147195
    148     public boolean readStart(String namespaceURI, String localName,
    149             boolean required, boolean attributes) throws XMLStreamException {
    150 //        System.err.println("readStart (" + localName + ", required = " +
    151 //                required + ") @ " + toReadable(reader));
     196
     197    boolean readStart(String namespaceURI, String localName, boolean required,
     198            boolean attributes) throws XMLStreamException {
     199        // System.err.println("readStart (" + localName + ", required = " +
     200        // required + ") @ " + toReadable(reader));
    152201        if (!reader.isEndElement()) {
    153202            while (reader.hasNext()) {
    154203//                System.err.println("  LOOP: " + dumpState());
    155                 if (reader.isWhiteSpace()) {
    156                     reader.next();
    157                     continue;
    158                 }
     204                consumeWhitespace();
    159205                if (reader.isStartElement()) {
    160206                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
    161207                            localName.equals(reader.getLocalName())) {
    162 //                        System.err.print("--> found ");
     208                        // System.err.print("--> found ");
    163209                        if (!attributes) {
    164 //                            System.err.print("and consumed ");
     210                            // System.err.print("and consumed ");
    165211                            reader.next(); // skip to next event
    166212                        }
    167 //                        System.err.println("@ " + toReadable(reader));
     213                        // System.err.println("@ " + toReadable(reader));
    168214                        return true;
    169215                    }
     
    177223        }
    178224        if (required) {
    179 //            System.err.println("--> error, not found @ " + toReadable(reader));
    180             throw new XMLStreamException("expected element '" +
    181                     new QName(namespaceURI, localName) + "', but found '" +
    182                     reader.getName() + "'", reader.getLocation());
    183         }
    184 //        System.err.println("--> not found @ " + toReadable(reader));
     225            logger.error("fail: {}", dumpState());
     226
     227            // System.err.println("--> error, not found @ " +
     228            // toReadable(reader));
     229            if (reader.hasName()) {
     230                throw new XMLStreamException("expected element '" +
     231                        new QName(namespaceURI, localName) + "', but found '" +
     232                        reader.getName() + "'", reader.getLocation());
     233            } else {
     234                throw new XMLStreamException("unexpacted state while scanning for start tag for element '" + new QName(namespaceURI, localName) + "'", reader.getLocation());
     235            }
     236        }
     237        // System.err.println("--> not found @ " + toReadable(reader));
    185238        return false;
    186239    }
    187240
    188     public void readEnd(String namespaceURI, String localName)
     241
     242    void readEnd(String namespaceURI, String localName)
    189243            throws XMLStreamException {
    190244        readEnd(namespaceURI, localName, false);
    191245    }
    192246
    193     public void readEnd(String namespaceURI, String localName,
    194             boolean skipContent) throws XMLStreamException {
    195 //        System.err.println("readEnd (" + localName + ") @ " + dumpState() +
    196 //                ", skipContent = " + skipContent);
     247
     248    void readEnd(String namespaceURI, String localName, boolean skipContent)
     249            throws XMLStreamException {
     250        // System.err.println("readEnd (" + localName + ") @ " + dumpState() +
     251        // ", skipContent = " + skipContent);
    197252        int level = 1;
    198253        while (reader.hasNext()) {
    199 //            System.err.println("  LOOP " + dumpState() + " [" +
    200 //                    level + "]");
    201             if (reader.isWhiteSpace()) {
    202                 reader.next();
    203                 continue;
    204             }
     254            // System.err.println("  LOOP " + dumpState() + " [" +
     255            // level + "]");
     256            consumeWhitespace();
    205257            if (skipContent) {
    206258                if (reader.isCharacters()) {
     
    219271            if (reader.isEndElement()) {
    220272                level--;
    221 //                System.err.println("   @END-TAG: " + dumpState() + " [" +
    222 //                        level + "]");
     273                // System.err.println("   @END-TAG: " + dumpState() + " [" +
     274                // level + "]");
    223275                if (level == 0) {
    224276                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
    225                         localName.equals(reader.getLocalName())) {
     277                            localName.equals(reader.getLocalName())) {
    226278                        reader.next(); // consume tag
    227279                        break;
     
    236288            reader.next();
    237289        }
    238 //        System.err.println("--> ok @ " + dumpState());
    239     }
    240 
    241     public String readContent(String namespaceURI, String localName,
    242             boolean required) throws XMLStreamException {
     290        // System.err.println("--> ok @ " + dumpState());
     291    }
     292
     293
     294    boolean peekStart(String namespaceURI, String localName)
     295            throws XMLStreamException {
     296        if (!reader.isEndElement()) {
     297            while (reader.hasNext()) {
     298                // System.err.println("  LOOP: " + dumpState());
     299                if (reader.isWhiteSpace()) {
     300                    reader.next();
     301                    continue;
     302                }
     303                if (reader.isStartElement()) {
     304                    if (namespaceURI.equals(reader.getNamespaceURI()) &&
     305                            localName.equals(reader.getLocalName())) {
     306                        return true;
     307                    } else {
     308                        return false;
     309                    }
     310                }
     311                if (reader.isCharacters() || reader.isEndElement()) {
     312                    break;
     313                }
     314                reader.next();
     315            } // while
     316        }
     317        return false;
     318    }
     319
     320
     321    String readContent(String namespaceURI, String localName, boolean required)
     322            throws XMLStreamException {
     323        return readContent(namespaceURI, localName, required, true);
     324    }
     325
     326
     327    String readContent(String namespaceURI, String localName, boolean required,
     328            boolean contentRequired) throws XMLStreamException {
    243329        String result = null;
    244330        if (readStart(namespaceURI, localName, required)) {
    245             result = readString(true);
     331            try {
     332                result = readString(contentRequired);
     333                if (!contentRequired && (result == null)) {
     334                    result = "";
     335                }
     336            } catch (XMLStreamException e) {
     337                StringBuilder sb = new StringBuilder();
     338                sb.append("element '");
     339                sb.append(new QName(namespaceURI, localName));
     340                sb.append(localName).append("' may not be empty");
     341                throw new XMLStreamException(sb.toString(), e.getLocation());
     342            }
    246343            readEnd(namespaceURI, localName);
    247344        }
     
    249346    }
    250347
    251     public String readString(boolean required)
    252             throws XMLStreamException {
    253 //        System.err.println("readString @ " + toReadable(reader));
     348
     349    int readContent(String namespaceURI, String localName, boolean required,
     350            int defaultValue) throws XMLStreamException {
     351        if (readStart(namespaceURI, localName, required)) {
     352            String s = readString(true);
     353            try {
     354                readEnd(namespaceURI, localName);
     355            } catch (XMLStreamException e) {
     356                StringBuilder sb = new StringBuilder();
     357                sb.append("element '");
     358                sb.append(new QName(namespaceURI, localName));
     359                sb.append(localName).append("' may not be empty");
     360                throw new XMLStreamException(sb.toString(), e.getLocation());
     361            }
     362            try {
     363                return Integer.parseInt(s);
     364            } catch (NumberFormatException e) {
     365                StringBuilder sb = new StringBuilder();
     366                sb.append("element '");
     367                sb.append(new QName(namespaceURI, localName));
     368                sb.append("' was expected to be of type xs:integer; ");
     369                sb.append("incompatible value was: ");
     370                sb.append(s);
     371                throw new XMLStreamException(sb.toString(),
     372                        reader.getLocation(), e);
     373            }
     374        }
     375        return defaultValue;
     376    }
     377
     378
     379    String readString(boolean required) throws XMLStreamException {
     380        // System.err.println("readString @ " + toReadable(reader));
     381        StringBuilder sb = new StringBuilder();
     382        while (reader.isCharacters()) {
     383            String s = reader.getText();
     384            if (s != null) {
     385                sb.append(s);
     386            }
     387            reader.next();
     388        } // while
    254389        String s = null;
    255         if (reader.isCharacters()) {
    256             s = reader.getText();
    257             if (s != null) {
    258                 s = s.trim();
    259             }
    260             reader.next();
     390        if (sb.length() > 0) {
     391            s = sb.toString().trim();
    261392        }
    262393        if (required && ((s == null) || s.isEmpty())) {
    263394            throw new XMLStreamException("expected character content "
    264                     + "at position", reader.getLocation());
    265         }
    266 //      System.err.println("--> ok @ " + toReadable(reader));
     395                    + "at position ", reader.getLocation());
     396        }
     397        // System.err.println("--> ok @ " + toReadable(reader));
    267398        return s;
    268399    }
    269400
    270     public String readAttributeValue(String namespaceURI, String localName)
     401
     402    String readAttributeValue(String namespaceURI, String localName)
    271403            throws XMLStreamException {
    272404        if (!reader.isStartElement()) {
     
    281413    }
    282414
    283     public String readNamespaceURI() throws XMLStreamException {
     415
     416    String readNamespaceURI() throws XMLStreamException {
    284417        if (!reader.isStartElement()) {
    285418            throw new XMLStreamException("not at a start elment event",
     
    289422    }
    290423
    291     public String peekElementLocalName() throws XMLStreamException {
     424
     425    String peekElementLocalName() throws XMLStreamException {
    292426        if (!reader.isStartElement()) {
    293427            throw new XMLStreamException("not at a start elment event",
     
    297431    }
    298432
    299     public void consumeStart() throws XMLStreamException {
     433
     434    void consumeStart() throws XMLStreamException {
    300435        if (!reader.isStartElement()) {
    301436            throw new XMLStreamException("not at a start elment event",
     
    305440    }
    306441
    307     public void consumeWhitespace() throws XMLStreamException {
    308         while (reader.isWhiteSpace() && reader.hasNext()) {
     442
     443    void consumeWhitespace() throws XMLStreamException {
     444        outer:
     445        while (reader.hasNext()) {
     446            /*
     447             * some hackish method to also skip stray UTF-8 BOMs; only works
     448             * where whitespace is expected
     449             */
     450            if (reader.isCharacters()) {
     451                final char buffer[] = reader.getTextCharacters();
     452                final int end = reader.getTextStart() + reader.getTextLength();
     453                for (int i = reader.getTextStart(); i < end; i++) {
     454                    if (!(Character.isWhitespace(buffer[i]) ||
     455                            (buffer[i] == 0x0FEFF))) {
     456                        break outer;
     457                    }
     458                }
     459            } else if (!reader.isWhiteSpace()) {
     460                break outer;
     461            }
    309462            reader.next();
    310             continue;
    311         }
    312     }
    313 
    314     public XMLStreamReader getXMLStreamReader() {
    315         return reader;
    316     }
    317 
    318     public String dumpState() {
     463        } // while
     464    }
     465
     466
     467    void copyTo(XMLStreamWriter writer) throws XMLStreamException {
     468        final int depth = reader.getDepth();
     469        do {
     470            copyEvent(reader, writer);
     471            reader.next();
     472        } while (reader.getDepth() >= depth);
     473    }
     474
     475
     476    private static void copyEvent(XMLStreamReader from, XMLStreamWriter to)
     477            throws XMLStreamException {
     478        switch (from.getEventType()) {
     479        case XMLStreamConstants.START_DOCUMENT:
     480            {
     481                String version = from.getVersion();
     482                if (version == null || version.length() == 0) {
     483                    to.writeStartDocument();
     484                } else {
     485                    to.writeStartDocument(from.getCharacterEncodingScheme(),
     486                            from.getVersion());
     487                }
     488                to.writeCharacters("\n");
     489            }
     490            return;
     491
     492        case XMLStreamConstants.END_DOCUMENT:
     493            to.writeCharacters("\n");
     494            to.writeEndDocument();
     495            return;
     496
     497        case XMLStreamConstants.START_ELEMENT:
     498            copyStartElement(from, to);
     499            return;
     500
     501        case XMLStreamConstants.END_ELEMENT:
     502            to.writeEndElement();
     503            return;
     504
     505        case XMLStreamConstants.SPACE:
     506            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
     507                    from.getTextLength());
     508            return;
     509
     510        case XMLStreamConstants.CDATA:
     511            to.writeCData(from.getText());
     512            return;
     513
     514        case XMLStreamConstants.CHARACTERS:
     515            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
     516                    from.getTextLength());
     517            return;
     518
     519        case XMLStreamConstants.COMMENT:
     520            to.writeComment(from.getText());
     521            return;
     522
     523        case XMLStreamConstants.PROCESSING_INSTRUCTION:
     524            to.writeProcessingInstruction(from.getPITarget(), from.getPIData());
     525            return;
     526
     527        case XMLStreamConstants.DTD:
     528        case XMLStreamConstants.ENTITY_REFERENCE:
     529        case XMLStreamConstants.ATTRIBUTE:
     530        case XMLStreamConstants.NAMESPACE:
     531        case XMLStreamConstants.ENTITY_DECLARATION:
     532        case XMLStreamConstants.NOTATION_DECLARATION:
     533            /* FALL_TROUGH */
     534        }
     535        throw new XMLStreamException("unsupported event type: " +
     536                from.getEventType());
     537    }
     538
     539
     540    private static void copyStartElement(XMLStreamReader from,
     541            XMLStreamWriter to) throws XMLStreamException {
     542        final int nsCount = from.getNamespaceCount();
     543        if (nsCount > 0) { // yup, got some...
     544            for (int i = 0; i < nsCount; ++i) {
     545                String pfx = from.getNamespacePrefix(i);
     546                String uri = from.getNamespaceURI(i);
     547                if ((pfx == null) || pfx.isEmpty()) { // default NS
     548                    to.setDefaultNamespace(uri);
     549                } else {
     550                    to.setPrefix(pfx, uri);
     551                }
     552            }
     553        }
     554
     555        final String prefix = from.getPrefix();
     556        final NamespaceContext from_ctx = from.getNamespaceContext();
     557        final NamespaceContext to_ctx = to.getNamespaceContext();
     558        boolean repair_prefix_namespace = false;
     559        if ((prefix != null) && (to_ctx.getNamespaceURI(prefix) == null)) {
     560            repair_prefix_namespace = true;
     561            to.setPrefix(prefix, from_ctx.getNamespaceURI(prefix));
     562        }
     563
     564        to.writeStartElement(prefix, from.getLocalName(),
     565                from.getNamespaceURI());
     566
     567        if (nsCount > 0) {
     568            // write namespace declarations
     569            for (int i = 0; i < nsCount; ++i) {
     570                String pfx = from.getNamespacePrefix(i);
     571                String uri = from.getNamespaceURI(i);
     572
     573                if ((pfx == null) || pfx.isEmpty()) { // default NS
     574                    to.writeDefaultNamespace(uri);
     575                } else {
     576                    to.writeNamespace(pfx, uri);
     577                }
     578            }
     579        }
     580        if (repair_prefix_namespace) {
     581            to.writeNamespace(prefix, from_ctx.getNamespaceURI(prefix));
     582        }
     583
     584        int attrCount = from.getAttributeCount();
     585        if (attrCount > 0) {
     586            for (int i = 0; i < attrCount; ++i) {
     587                to.writeAttribute(from.getAttributePrefix(i),
     588                        from.getAttributeNamespace(i),
     589                        from.getAttributeLocalName(i),
     590                        from.getAttributeValue(i));
     591            }
     592        }
     593    }
     594
     595
     596    String dumpState() {
    319597        StringBuilder sb = new StringBuilder();
    320598        switch (reader.getEventType()) {
    321599        case XMLStreamConstants.START_DOCUMENT:
    322             return "START_DOC";
     600            return "START_DOC[";
    323601        case XMLStreamConstants.END_DOCUMENT:
    324             return "END_DOC";
     602            return "END_DOC[";
    325603        case XMLStreamConstants.START_ELEMENT:
    326604            sb.append("START[");
     
    328606            sb.append(",");
    329607            sb.append(reader.getLocalName());
    330             sb.append("]");
    331608            break;
    332609        case XMLStreamConstants.END_ELEMENT:
     
    335612            sb.append(",");
    336613            sb.append(reader.getLocalName());
    337             sb.append("]");
    338614            break;
    339615        case XMLStreamConstants.CHARACTERS:
    340616            sb.append("CHARACTERS[\"");
    341             sb.append(reader.getText().replace("\n", "\\n")
    342                     .replace("\r", "\\r").replace("\t", "\\t"));
     617            sb.append(reader.getText()
     618                    .replace("\n", "\\n")
     619                    .replace("\r", "\\r")
     620                    .replace("\t", "\\t"));
    343621            sb.append("\", isWhitespace = ");
    344622            sb.append(reader.isWhiteSpace());
    345             sb.append("]");
    346623            break;
    347624        case XMLStreamConstants.CDATA:
     
    351628            sb.append("\", isWhitespace = ");
    352629            sb.append(reader.isWhiteSpace());
    353             sb.append("]");
    354630            break;
    355631        default:
    356632            sb.append(Integer.toString(reader.getEventType()));
    357633        }
     634        Location location = reader.getLocation();
     635        sb.append(", row = ").append(location.getLineNumber());
     636        sb.append(", col = ").append(location.getColumnNumber());
     637        sb.append(", offset = ").append(location.getCharacterOffset());
     638        sb.append("]");
    358639        return sb.toString();
    359640    }
    360    
     641
    361642} // class XmlStreamReaderWrapper
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/SimpleHarvester.java

    r1148 r3011  
    33import java.net.URI;
    44import java.util.concurrent.TimeUnit;
     5import java.util.concurrent.atomic.AtomicLong;
     6
     7import org.slf4j.Logger;
     8import org.slf4j.LoggerFactory;
    59
    610import eu.clarin.cmdi.oai.harvester.HarvestHandler;
     
    913import eu.clarin.cmdi.oai.harvester.HarvesterException;
    1014
     15
    1116public final class SimpleHarvester extends AbstractHarvester {
    12     private HarvestWorker worker;
    13     private long nextId = 0;
     17    private static final Logger logger =
     18            LoggerFactory.getLogger(SimpleHarvester.class);
     19    private final AtomicLong nextId = new AtomicLong(0);
     20    private final HarvestWorker worker;
     21
    1422
    1523    private SimpleHarvester() {
    1624        this.worker = new HarvestWorker(this);
    1725    }
     26
    1827
    1928    @Override
     
    2231            throw new IllegalArgumentException("repositoryURI == null");
    2332        }
    24         long jobId;
    25         synchronized (this) {
    26             jobId = ++nextId;
    27         } // synchronized
    28         return new HarvestJobImpl(this, jobId, repositoryURI, handler);
     33        return new HarvestJobImpl(this, nextId.incrementAndGet(),
     34                repositoryURI, handler);
    2935    }
     36
    3037
    3138    public static Harvester newInstance() {
     
    3340    }
    3441
     42
    3543    @Override
    3644    public void shutdown() {
    3745    }
     46
    3847
    3948    @Override
     
    5261                    ((delay = job.getDelay(TimeUnit.MILLISECONDS)) > 0)) {
    5362                try {
    54                     System.err.println(job.getId() + ": (" +
    55                             job.getNetworkRetryCount() + ") waiting " +
    56                             delay + " ...");
    57                     /* add a few milliseconds, so we do not loop  */
     63                    logger.debug("{}: ({}) waiting {} ...",
     64                            job.getId(), job.getNetworkRetryCount(), delay);
     65                    /* add a few milliseconds, so we do not loop */
    5866                    Thread.sleep(delay + 25);
    5967                } catch (InterruptedException e) {
     
    6472    }
    6573
     74
    6675    @Override
    6776    void doCancelJob(HarvestJobImpl job) throws HarvesterException {
     
    6978    }
    7079
     80
    7181    @Override
    7282    protected HarvestJob doFindJob(long id) throws HarvesterException {
    73         throw new HarvesterException("findJob() is not supported"); 
     83        throw new HarvesterException("findJob() is not supported");
    7484    }
    7585
  • OAIHarvester/trunk/OAIHarvester/src/main/java/eu/clarin/cmdi/oai/harvester/impl/XmlStreamReaderProxy.java

    r1128 r3011  
    1515    private State state;
    1616    private int depth;
    17    
     17
    1818    XmlStreamReaderProxy() {
    1919        super();
     
    3535                XMLStreamConstants.ATTRIBUTE)) {
    3636            throw new IllegalStateException(
    37                     "Current event is not START_ELEMENT or ATTRIBUTE");
     37                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    3838        }
    3939        return reader.getAttributeCount();
     
    4545                XMLStreamConstants.ATTRIBUTE)) {
    4646            throw new IllegalStateException(
    47                     "Current event is not START_ELEMENT or ATTRIBUTE");
     47                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    4848        }
    4949        return reader.getAttributeLocalName(index);
     
    5555                XMLStreamConstants.ATTRIBUTE)) {
    5656            throw new IllegalStateException(
    57                     "Current event is not START_ELEMENT or ATTRIBUTE");
     57                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    5858        }
    5959        return reader.getAttributeName(index);
     
    6565                XMLStreamConstants.ATTRIBUTE)) {
    6666            throw new IllegalStateException(
    67                     "Current event is not START_ELEMENT or ATTRIBUTE");
     67                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    6868        }
    6969        return reader.getAttributeNamespace(index);
     
    7575                XMLStreamConstants.ATTRIBUTE)) {
    7676            throw new IllegalStateException(
    77                     "Current event is not START_ELEMENT or ATTRIBUTE");
     77                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    7878        }
    7979        return reader.getAttributePrefix(index);
     
    8585                XMLStreamConstants.ATTRIBUTE)) {
    8686            throw new IllegalStateException(
    87                     "Current event is not START_ELEMENT or ATTRIBUTE");
     87                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    8888        }
    8989        return reader.getAttributeType(index);
     
    9595                XMLStreamConstants.ATTRIBUTE)) {
    9696            throw new IllegalStateException(
    97                     "Current event is not START_ELEMENT or ATTRIBUTE");
     97                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    9898        }
    9999        return reader.getAttributeValue(index);
     
    105105                XMLStreamConstants.ATTRIBUTE)) {
    106106            throw new IllegalStateException(
    107                     "Current event is not START_ELEMENT or ATTRIBUTE");
     107                    "Current event is not START_ELEMENT or ATTRIBUTE: " + reader.getLocation());
    108108        }
    109109        return reader.getAttributeValue(namespaceURI, localName);
     
    119119        if (!checkEventType(XMLStreamConstants.START_ELEMENT)) {
    120120            throw new IllegalStateException(
    121                     "Current event is not START_ELEMENT");
     121                    "Current event is not START_ELEMENT: " + reader.getLocation());
    122122        }
    123123        return reader.getElementText();
     
    166166                XMLStreamConstants.END_ELEMENT)) {
    167167            throw new IllegalStateException("Current event is not " +
    168                     "START_ELEMENT or END_ELEMENT");
     168                    "START_ELEMENT or END_ELEMENT: " + reader.getLocation());
    169169        }
    170170        return reader.getName();
     
    176176                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
    177177            throw new IllegalStateException("Current event is not " +
    178                     "START_ELEMENT, END_ELEMENT or NAMESPACE");
     178                    "START_ELEMENT, END_ELEMENT or NAMESPACE: " + reader.getLocation());
    179179        }
    180180        return reader.getNamespaceContext();
     
    186186                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
    187187            throw new IllegalStateException("Current event is not " +
    188                     "START_ELEMENT, END_ELEMENT or NAMESPACE");
     188                    "START_ELEMENT, END_ELEMENT or NAMESPACE: " + reader.getLocation());
    189189        }
    190190        return reader.getNamespaceCount();
     
    196196                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
    197197            throw new IllegalStateException("Current event is not " +
    198                     "START_ELEMENT, END_ELEMENT or NAMESPACE");
     198                    "START_ELEMENT, END_ELEMENT or NAMESPACE: " + reader.getLocation());
    199199        }
    200200        return reader.getNamespacePrefix(index);
     
    206206                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
    207207            throw new IllegalStateException("Current event is not " +
    208                     "START_ELEMENT, END_ELEMENT or NAMESPACE");
     208                    "START_ELEMENT, END_ELEMENT or NAMESPACE: " + reader.getLocation());
    209209        }
    210210        return reader.getNamespaceURI();
     
    221221                XMLStreamConstants.END_ELEMENT, XMLStreamConstants.NAMESPACE)) {
    222222            throw new IllegalStateException("Current event is not " +
    223                     "START_ELEMENT, END_ELEMENT or NAMESPACE");
     223                    "START_ELEMENT, END_ELEMENT or NAMESPACE: " + reader.getLocation());
    224224        }
    225225        return reader.getNamespaceURI(index);
     
    230230        if (!checkEventType(XMLStreamConstants.PROCESSING_INSTRUCTION)) {
    231231            throw new IllegalStateException(
    232                     "Current event is not PROCESSING_INSTRUCTION");
     232                    "Current event is not PROCESSING_INSTRUCTION: " + reader.getLocation());
    233233        }
    234234        return reader.getPIData();
     
    239239        if (!checkEventType(XMLStreamConstants.PROCESSING_INSTRUCTION)) {
    240240            throw new IllegalStateException(
    241                     "Current event is not PROCESSING_INSTRUCTION");
     241                    "Current event is not PROCESSING_INSTRUCTION: " + reader.getLocation());
    242242        }
    243243        return reader.getPITarget();
     
    445445                break;
    446446            default:
    447                 ;
     447                break;
    448448            } // switch (t)
    449449            if (depth < 0) {
  • OAIHarvester/trunk/OAIHarvester/src/test/java/eu/clarin/cmdi/oai/harvester/HarvesterTest.java

    r1906 r3011  
    1515import javax.xml.stream.XMLOutputFactory;
    1616import javax.xml.stream.XMLStreamConstants;
     17import javax.xml.stream.XMLStreamException;
    1718import javax.xml.stream.XMLStreamReader;
    1819import javax.xml.stream.XMLStreamWriter;
     
    105106            }
    106107        }
    107        
     108
    108109        @Override
    109110        public void onListMetadataFormats(List<MetadataFormat> metadataFormats) {
     
    182183                    output.flush();
    183184                }
    184             } catch (Exception e) {
     185            } catch (IOException e) {
     186                e.printStackTrace();
     187                throw new RuntimeException("record failed");
     188            } catch (XMLStreamException e) {
    185189                e.printStackTrace();
    186190                throw new RuntimeException("record failed");
     
    275279//            cal.set(2010, Calendar.DECEMBER, 15);
    276280//            job.setFrom(cal.getTime());
    277 //            job.setMetadataPrefixes(Arrays.asList("oai_dc", "cmdi"));
     281//            job.setMetadataPrefixes(Arrays.asList("cmdi"));
    278282            job.run();
    279283
     284            /*
     285             * Bad example of (busy) wait loop; should be done better, e.g.
     286             * with CountDownLatch ...
     287             */
    280288            while (job.isRunning()) {
    281289                try {
     
    372380    }
    373381
     382
     383    static {
     384        /*
     385         * quick and dirty setup for Log4J; don't copy blindly!
     386         */
     387        org.apache.log4j.BasicConfigurator
     388                .configure(new org.apache.log4j.ConsoleAppender(
     389                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
     390                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
     391        org.apache.log4j.Logger logger =
     392                org.apache.log4j.Logger.getRootLogger();
     393        logger.setLevel(org.apache.log4j.Level.INFO);
     394        logger.getLoggerRepository().getLogger("eu.clarin")
     395                .setLevel(org.apache.log4j.Level.DEBUG);
     396    }
     397
    374398} // class HarvesterTest
Note: See TracChangeset for help on using the changeset viewer.