private Jp2File getOpenJ2pFile(File outputFile) throws IOException {
      Jp2File jp2File = openFiles.get(outputFile);
      if (jp2File == null) {
        jp2File = new Jp2File();
        jp2File.file = outputFile;
        jp2File.stream = new FileImageInputStream(outputFile);
        jp2File.header = jp2File.stream.readLine();
        jp2File.dataPos = jp2File.stream.getStreamPosition();

        final String[] tokens = jp2File.header.split(" ");
        if (tokens.length != 6) {
          throw new IOException("Unexpected tile format");
        }

        // String pg = tokens[0];   // PG
        // String ml = tokens[1];   // ML
        // String plus = tokens[2]; // +
        int jp2Width;
        int jp2Height;
        try {
          // int jp2File.nbits = Integer.parseInt(tokens[3]);
          jp2File.width = Integer.parseInt(tokens[4]);
          jp2File.height = Integer.parseInt(tokens[5]);
        } catch (NumberFormatException e) {
          throw new IOException("Unexpected tile format");
        }

        openFiles.put(outputFile, jp2File);
      }

      return jp2File;
    }
    private void readTileData(
        File outputFile,
        int tileX,
        int tileY,
        int tileWidth,
        int tileHeight,
        int jp2TileX,
        int jp2TileY,
        int jp2TileWidth,
        int jp2TileHeight,
        short[] tileData,
        Rectangle destRect)
        throws IOException {

      synchronized (this) {
        if (!locks.containsKey(outputFile)) {
          locks.put(outputFile, new Object());
        }
      }
      final Object lock = locks.get(outputFile);

      synchronized (lock) {
        Jp2File jp2File = getOpenJ2pFile(outputFile);

        int jp2Width = jp2File.width;
        int jp2Height = jp2File.height;
        if (jp2Width > jp2TileWidth || jp2Height > jp2TileHeight) {
          throw new IllegalStateException(
              String.format(
                  "width (=%d) > tileWidth (=%d) || height (=%d) > tileHeight (=%d)",
                  jp2Width, jp2TileWidth, jp2Height, jp2TileHeight));
        }

        int jp2X = destRect.x - jp2TileX * jp2TileWidth;
        int jp2Y = destRect.y - jp2TileY * jp2TileHeight;
        if (jp2X < 0 || jp2Y < 0) {
          throw new IllegalStateException(
              String.format("jp2X (=%d) < 0 || jp2Y (=%d) < 0", jp2X, jp2Y));
        }

        final ImageInputStream stream = jp2File.stream;

        if (jp2X == 0
            && jp2Width == tileWidth
            && jp2Y == 0
            && jp2Height == tileHeight
            && tileWidth * tileHeight == tileData.length) {
          stream.seek(jp2File.dataPos);
          stream.readFully(tileData, 0, tileData.length);
        } else {
          final Rectangle jp2FileRect = new Rectangle(0, 0, jp2Width, jp2Height);
          final Rectangle tileRect = new Rectangle(jp2X, jp2Y, tileWidth, tileHeight);
          final Rectangle intersection = jp2FileRect.intersection(tileRect);
          System.out.printf(
              "%s: tile=(%d,%d): jp2FileRect=%s, tileRect=%s, intersection=%s\n",
              jp2File.file, tileX, tileY, jp2FileRect, tileRect, intersection);
          if (!intersection.isEmpty()) {
            long seekPos =
                jp2File.dataPos + NUM_SHORT_BYTES * (intersection.y * jp2Width + intersection.x);
            int tilePos = 0;
            for (int y = 0; y < intersection.height; y++) {
              stream.seek(seekPos);
              stream.readFully(tileData, tilePos, intersection.width);
              seekPos += NUM_SHORT_BYTES * jp2Width;
              tilePos += tileWidth;
              for (int x = intersection.width; x < tileWidth; x++) {
                tileData[y * tileWidth + x] = (short) 0;
              }
            }
            for (int y = intersection.height; y < tileWidth; y++) {
              for (int x = 0; x < tileWidth; x++) {
                tileData[y * tileWidth + x] = (short) 0;
              }
            }
          } else {
            Arrays.fill(tileData, (short) 0);
          }
        }
      }
    }
  @Override
  protected Product readProductNodesImpl() throws IOException {
    final String s = getInput().toString();

    final File file0 = new File(s);
    final File dir = file0.getParentFile();

    final S2FilenameInfo fni0 = S2FilenameInfo.create(file0.getName());
    if (fni0 == null) {
      throw new IOException();
    }
    Header metadataHeader = null;
    final Map<Integer, BandInfo> fileMap = new HashMap<Integer, BandInfo>();
    if (dir != null) {
      File[] files =
          dir.listFiles(
              new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                  return name.endsWith(Sentinel2ProductReaderPlugIn.JP2_EXT);
                }
              });
      if (files != null) {
        for (File file : files) {
          int bandIndex = fni0.getBand(file.getName());
          if (bandIndex >= 0 && bandIndex < WAVEBAND_INFOS.length) {
            final S2WavebandInfo wavebandInfo = WAVEBAND_INFOS[bandIndex];
            BandInfo bandInfo =
                new BandInfo(
                    file, bandIndex, wavebandInfo, imageLayouts[wavebandInfo.resolution.id]);
            fileMap.put(bandIndex, bandInfo);
          }
        }
      }
      File[] metadataFiles =
          dir.listFiles(
              new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                  return name.startsWith("MTD_") && name.endsWith(".xml");
                }
              });
      if (metadataFiles != null && metadataFiles.length > 0) {
        File metadataFile = metadataFiles[0];
        try {
          metadataHeader = Header.parseHeader(metadataFile);
        } catch (JDOMException e) {
          BeamLogManager.getSystemLogger()
              .warning("Failed to parse metadata file: " + metadataFile);
        }
      } else {
        BeamLogManager.getSystemLogger().warning("No metadata file found");
      }
    }

    final ArrayList<Integer> bandIndexes = new ArrayList<Integer>(fileMap.keySet());
    Collections.sort(bandIndexes);

    if (bandIndexes.isEmpty()) {
      throw new IOException("No valid bands found.");
    }

    String prodType = "S2_MSI_" + fni0.procLevel;
    final Product product =
        new Product(
            String.format("%s_%s_%s", prodType, fni0.orbitNo, fni0.tileId),
            prodType,
            imageLayouts[S2Resolution.R10M.id].width,
            imageLayouts[S2Resolution.R10M.id].height);

    try {
      product.setStartTime(ProductData.UTC.parse(fni0.start, "yyyyMMddHHmmss"));
    } catch (ParseException e) {
      // warn
    }

    try {
      product.setEndTime(ProductData.UTC.parse(fni0.stop, "yyyyMMddHHmmss"));
    } catch (ParseException e) {
      // warn
    }

    if (metadataHeader != null) {
      SceneDescription sceneDescription = SceneDescription.create(metadataHeader);
      int tileIndex = sceneDescription.getTileIndex(fni0.tileId);
      Envelope2D tileEnvelope = sceneDescription.getTileEnvelope(tileIndex);
      Header.Tile tile = metadataHeader.getTileList().get(tileIndex);

      try {
        product.setGeoCoding(
            new CrsGeoCoding(
                tileEnvelope.getCoordinateReferenceSystem(),
                imageLayouts[S2Resolution.R10M.id].width,
                imageLayouts[S2Resolution.R10M.id].height,
                tile.tileGeometry10M.upperLeftX,
                tile.tileGeometry10M.upperLeftY,
                tile.tileGeometry10M.xDim,
                -tile.tileGeometry10M.yDim,
                0.0,
                0.0));
      } catch (FactoryException e) {
        // todo - handle e
      } catch (TransformException e) {
        // todo - handle e
      }
    }

    for (Integer bandIndex : bandIndexes) {
      final BandInfo bandInfo = fileMap.get(bandIndex);
      final Band band = product.addBand(bandInfo.wavebandInfo.bandName, ProductData.TYPE_UINT16);
      band.setSpectralWavelength((float) bandInfo.wavebandInfo.centralWavelength);
      band.setSpectralBandwidth((float) bandInfo.wavebandInfo.bandWidth);
      band.setSpectralBandIndex(bandIndex);
      band.setSourceImage(new DefaultMultiLevelImage(new Jp2MultiLevelSource(bandInfo)));
    }

    product.setNumResolutionLevels(imageLayouts[0].numResolutions);

    return product;
  }