485 lines
25 KiB
Plaintext
485 lines
25 KiB
Plaintext
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
|
||
<cfheader name="Content-Type" value="application/json; charset=utf-8">
|
||
|
||
<cfparam name="url.doc_id" default="">
|
||
|
||
<!--- ═══════════════════════════════════════════════════════════
|
||
Helper: parsePDF — PDFBox → {elements, errors}
|
||
═══════════════════════════════════════════════════════════ --->
|
||
<cffunction name="parsePDF" access="private" returntype="struct">
|
||
<cfargument name="fileBytes" type="binary">
|
||
<cfset var out = {elements: [], errors: []}>
|
||
<cftry>
|
||
<cfset var pdfDoc = 0>
|
||
<cftry>
|
||
<cfset pdfDoc = createObject("java", "org.apache.pdfbox.Loader").loadPDF(fileBytes)>
|
||
<cfcatch>
|
||
<cftry>
|
||
<cfset pdfDoc = createObject("java", "org.apache.pdfbox.pdmodel.PDDocument").load(fileBytes)>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, "PDFBox: cannot open — " & cfcatch.message)>
|
||
</cftry>
|
||
<cfif NOT isNumeric(pdfDoc)>
|
||
<cfset var stripper = createObject("java", "org.apache.pdfbox.text.PDFTextStripper")>
|
||
<cfset var pageCount = pdfDoc.getNumberOfPages()>
|
||
<cfloop from="1" to="#pageCount#" index="pn">
|
||
<cfset stripper.setStartPage(pn)>
|
||
<cfset stripper.setEndPage(pn)>
|
||
<cfset var pageText = stripper.getText(pdfDoc)>
|
||
<cfif len(trim(pageText))>
|
||
<cfloop list="#pageText#" index="line" delimiters="#chr(10)#">
|
||
<cfset line = trim(line)>
|
||
<cfif len(line)>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "paragraph">
|
||
<cfset el.style = "">
|
||
<cfset el.text = line>
|
||
<cfset el.page = pn>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
</cfloop>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfset pdfDoc.close()>
|
||
</cfif>
|
||
<cfset bis.close()>
|
||
<cfset bais.close()>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, "PDFBox: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn out>
|
||
</cffunction>
|
||
|
||
<!--- ═══════════════════════════════════════════════════════════
|
||
Helper: parseDOCX_POI — XWPFDocument → {elements, errors}
|
||
Точнее чем ZIP/XML: понимает объединённые ячейки, стили
|
||
═══════════════════════════════════════════════════════════ --->
|
||
<cffunction name="parseDOCX_POI" access="private" returntype="struct">
|
||
<cfargument name="fileBytes" type="binary">
|
||
<cfset var out = {elements: [], errors: []}>
|
||
<cftry>
|
||
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
|
||
<cfset var doc = createObject("java", "org.apache.poi.xwpf.usermodel.XWPFDocument").init(bais)>
|
||
<cfset var bodyElements = doc.getBodyElements()>
|
||
<cfloop from="0" to="#bodyElements.size() - 1#" index="bi">
|
||
<cfset var be = bodyElements.get(bi)>
|
||
<cfset var beClass = be.getClass().getName()>
|
||
<cfif beClass EQ "org.apache.poi.xwpf.usermodel.XWPFParagraph">
|
||
<cfset var para = be>
|
||
<cfset var text = para.getText()>
|
||
<cfset var trimText = trim(text)>
|
||
<cfif len(trimText) AND len(trimText) LTE 2000>
|
||
<cfset var style = "">
|
||
<cftry><cfset style = para.getStyle()><cfcatch></cfcatch></cftry>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "paragraph">
|
||
<cfset el.style = style>
|
||
<cfset el.text = trimText>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
<cfelseif beClass EQ "org.apache.poi.xwpf.usermodel.XWPFTable">
|
||
<cfset var table = be>
|
||
<cfset var rows = []>
|
||
<cfset var rowsArr = table.getRows()>
|
||
<cfloop from="0" to="#rowsArr.size() - 1#" index="ri">
|
||
<cfset var row = rowsArr.get(ri)>
|
||
<cfset var cells = []>
|
||
<cfset var tcs = row.getTableCells()>
|
||
<cfloop from="0" to="#tcs.size() - 1#" index="ci">
|
||
<cfset var cell = tcs.get(ci)>
|
||
<cfset var cellText = trim(cell.getText())>
|
||
<cfset arrayAppend(cells, cellText)>
|
||
</cfloop>
|
||
<cfif arrayLen(cells) GT 0>
|
||
<cfset arrayAppend(rows, cells)>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfif arrayLen(rows) GT 0>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "table">
|
||
<cfset el.rows = rows>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfset doc.close()>
|
||
<cfset bais.close()>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, "DOCX_POI: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn out>
|
||
</cffunction>
|
||
|
||
<!--- ═══════════════════════════════════════════════════════════
|
||
Helper: parseDOCX — пробует POI, fallback ZIP/XML
|
||
═══════════════════════════════════════════════════════════ --->
|
||
<cffunction name="parseDOCX" access="private" returntype="struct">
|
||
<cfargument name="fileBytes" type="binary">
|
||
<cfset var out = {elements: [], errors: []}>
|
||
<cftry>
|
||
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
|
||
<cfset var zis = createObject("java", "java.util.zip.ZipInputStream").init(bais)>
|
||
<cfset var xmlStr = "">
|
||
<cfloop condition="true">
|
||
<cfset var entry = zis.getNextEntry()>
|
||
<cfif isNull(entry)><cfbreak></cfif>
|
||
<cfif entry.getName() EQ "word/document.xml">
|
||
<cftry>
|
||
<cfset var xmlBytes = zis.readAllBytes()>
|
||
<cfset xmlStr = createObject("java", "java.lang.String").init(xmlBytes, "UTF-8")>
|
||
<cfcatch>
|
||
<cfset var baos = createObject("java", "java.io.ByteArrayOutputStream").init()>
|
||
<cfloop condition="true">
|
||
<cfset var b = zis.read()>
|
||
<cfif b LT 0><cfbreak></cfif>
|
||
<cfset baos.write(b)>
|
||
</cfloop>
|
||
<cfset xmlStr = createObject("java", "java.lang.String").init(baos.toByteArray(), "UTF-8")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfbreak>
|
||
</cfif>
|
||
<cfset zis.closeEntry()>
|
||
</cfloop>
|
||
<cfset zis.close()>
|
||
<cfset bais.close()>
|
||
<cfif NOT len(xmlStr)>
|
||
<cfset arrayAppend(out.errors, "word/document.xml not found")>
|
||
<cfelse>
|
||
<cfset var docXml = xmlParse(xmlStr)>
|
||
<cfset var body = "">
|
||
<cftry>
|
||
<cfset body = xmlSearch(docXml, "//:body")>
|
||
<cfcatch>
|
||
<cfset body = xmlSearch(docXml, "//*[local-name()='body']")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfif arrayLen(body) GT 0>
|
||
<cfset body = body[1]>
|
||
<cfloop array="#body.XmlChildren#" index="child">
|
||
<cfset var localName = child.XmlName>
|
||
<cfif localName EQ "p" OR right(localName, 2) EQ ":p">
|
||
<cfset var paraText = "">
|
||
<cftry>
|
||
<cfset var textNodes = xmlSearch(child, ".//:t")>
|
||
<cfcatch>
|
||
<cfset textNodes = xmlSearch(child, ".//*[local-name()='t']")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfloop array="#textNodes#" index="tn">
|
||
<cfset paraText &= tn.XmlText>
|
||
</cfloop>
|
||
<cfset paraText = trim(paraText)>
|
||
<cfif len(paraText)>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "paragraph">
|
||
<cfset el.style = "">
|
||
<cfset el.text = paraText>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
<cfelseif localName EQ "tbl" OR right(localName, 4) EQ ":tbl">
|
||
<cfset var rows = []>
|
||
<cftry>
|
||
<cfset var trNodes = xmlSearch(child, ".//:tr")>
|
||
<cfcatch>
|
||
<cfset trNodes = xmlSearch(child, ".//*[local-name()='tr']")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfloop array="#trNodes#" index="trNode">
|
||
<cfset var row = []>
|
||
<cftry>
|
||
<cfset var tcNodes = xmlSearch(trNode, ".//:tc")>
|
||
<cfcatch>
|
||
<cfset tcNodes = xmlSearch(trNode, ".//*[local-name()='tc']")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfloop array="#tcNodes#" index="tcNode">
|
||
<cfset var cellText = "">
|
||
<cftry>
|
||
<cfset var tNodes = xmlSearch(tcNode, ".//:t")>
|
||
<cfcatch>
|
||
<cfset tNodes = xmlSearch(tcNode, ".//*[local-name()='t']")>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfloop array="#tNodes#" index="tNode">
|
||
<cfset cellText &= tNode.XmlText>
|
||
</cfloop>
|
||
<cfset arrayAppend(row, trim(cellText))>
|
||
</cfloop>
|
||
<cfif arrayLen(row) GT 0>
|
||
<cfset arrayAppend(rows, row)>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfif arrayLen(rows) GT 0>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "table">
|
||
<cfset el.rows = rows>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfelse>
|
||
<cfset arrayAppend(out.errors, "body not found")>
|
||
</cfif>
|
||
</cfif>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, "DOCX: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn out>
|
||
</cffunction>
|
||
|
||
<!--- ═══════════════════════════════════════════════════════════
|
||
Helper: parseDOC_POI — HWPFDocument → {elements, errors}
|
||
═══════════════════════════════════════════════════════════ --->
|
||
<cffunction name="parseDOC_POI" access="private" returntype="struct">
|
||
<cfargument name="fileBytes" type="binary">
|
||
<cfset var out = {elements: [], errors: []}>
|
||
<cftry>
|
||
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
|
||
<cfset var doc = createObject("java", "org.apache.poi.hwpf.HWPFDocument").init(bais)>
|
||
<!--- WordExtractor: извлекает весь текст --->
|
||
<cfset var extractor = createObject("java", "org.apache.poi.hwpf.extractor.WordExtractor").init(doc)>
|
||
<cfset var paragraphs = extractor.getParagraphText()>
|
||
<cfloop from="0" to="#arrayLen(paragraphs) - 1#" index="pi">
|
||
<cfset var text = trim(paragraphs[pi])>
|
||
<cfif len(text)>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "paragraph">
|
||
<cfset el.style = "">
|
||
<cfset el.text = text>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfset doc.close()>
|
||
<cfset bais.close()>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, "DOC_POI: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn out>
|
||
</cffunction>
|
||
|
||
<!--- ═══════════════════════════════════════════════════════════
|
||
Helper: parseDOC — грубый экстрактор текста из .doc (fallback)
|
||
═══════════════════════════════════════════════════════════ --->
|
||
<cffunction name="parseDOC" access="private" returntype="struct">
|
||
<cfargument name="fileBytes" type="binary">
|
||
<cfset var out = {elements: [], errors: []}>
|
||
<cftry>
|
||
<cfset var raw = createObject("java", "java.lang.String").init(fileBytes, "windows-1251")>
|
||
<cfset var lines = raw.split("\n")>
|
||
<cfloop array="#lines#" index="var line">
|
||
<cfset line = trim(line)>
|
||
<cfif len(line) GT 3 AND (reFind("[а-яА-ЯёЁ]", line) OR reFind("\d{2,}", line))>
|
||
<cfset var el = structNew()>
|
||
<cfset el.type = "paragraph">
|
||
<cfset el.style = "">
|
||
<cfset el.text = line>
|
||
<cfset el.page = 0>
|
||
<cfset arrayAppend(out.elements, el)>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfcatch>
|
||
<cfset arrayAppend(out.errors, ".doc: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfreturn out>
|
||
</cffunction>
|
||
|
||
<!--- ═══════════════════════════════════════════════ MAIN --->
|
||
<cfset result = {ok: false, error: ""}>
|
||
<cftry>
|
||
<cfif NOT len(url.doc_id)>
|
||
<cfset result.error = "doc_id required">
|
||
<cfelse>
|
||
<cfquery name="doc" datasource="baza">
|
||
SELECT id, filename, mime_type, original_bytes, status
|
||
FROM documents WHERE id = <cfqueryparam value="#url.doc_id#" cfsqltype="cf_sql_varchar">
|
||
</cfquery>
|
||
<cfif doc.recordCount EQ 0>
|
||
<cfset result.error = "document not found">
|
||
<cfelse>
|
||
<cfset elements = []>
|
||
<cfset errors = []>
|
||
<cfset files = []>
|
||
|
||
<!--- ZIP: поиск PDF/DOCX во всех подпапках --->
|
||
<cfif doc.mime_type EQ "application/zip">
|
||
<cftry>
|
||
<cfset zipBytes = binaryDecode(doc.original_bytes, "base64")>
|
||
<cfset zbais = createObject("java", "java.io.ByteArrayInputStream").init(zipBytes)>
|
||
<cfset zzis = createObject("java", "java.util.zip.ZipInputStream").init(zbais)>
|
||
<cfloop condition="true">
|
||
<cfset zentry = zzis.getNextEntry()>
|
||
<cfif isNull(zentry)><cfbreak></cfif>
|
||
<cfset zname = zentry.getName()>
|
||
<cfif right(zname, 1) EQ "/">
|
||
<cfset zzis.closeEntry()><cfcontinue>
|
||
</cfif>
|
||
<cfset entryBytes = "">
|
||
<cftry>
|
||
<cfset entryBytes = zzis.readAllBytes()>
|
||
<cfcatch>
|
||
<cfset zbaos = createObject("java", "java.io.ByteArrayOutputStream").init()>
|
||
<cfloop condition="true">
|
||
<cfset zb = zzis.read()>
|
||
<cfif zb LT 0><cfbreak></cfif>
|
||
<cfset zbaos.write(zb)>
|
||
</cfloop>
|
||
<cfset entryBytes = zbaos.toByteArray()>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfset zext = listLast(zname, ".")>
|
||
<cfset parsed = {elements: [], errors: []}>
|
||
<cfif zext EQ "pdf">
|
||
<cfset parsed = parsePDF(entryBytes)>
|
||
<cfelseif zext EQ "docx">
|
||
<cfset parsed = parseDOCX_POI(entryBytes)>
|
||
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
|
||
<cfset parsed = parseDOCX(entryBytes)>
|
||
</cfif>
|
||
<cfelseif zext EQ "doc">
|
||
<!--- После конвертации это уже docx — пробуем XWPF первым --->
|
||
<cftry>
|
||
<cfset parsed = parseDOCX(entryBytes)>
|
||
<cfcatch>
|
||
<cfset parsed = {elements: [], errors: ["XWPF fallback: " & cfcatch.message]}>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfif arrayLen(parsed.elements) EQ 0>
|
||
<cfset parsed = parseDOC_POI(entryBytes)>
|
||
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
|
||
<cfset parsed = parseDOC(entryBytes)>
|
||
</cfif>
|
||
</cfif>
|
||
<cfelse>
|
||
<cfset arrayAppend(parsed.errors, "unsupported: .#zext#")>
|
||
</cfif>
|
||
<cfset prefix = doc.filename & "/">
|
||
<cfloop array="#parsed.elements#" index="el">
|
||
<cfset el.archive_path = prefix & zname>
|
||
<cfset arrayAppend(elements, el)>
|
||
</cfloop>
|
||
<cfset arrayAppend(files, {
|
||
filename: zname, archive_path: prefix & zname,
|
||
element_count: arrayLen(parsed.elements),
|
||
error_count: arrayLen(parsed.errors)
|
||
})>
|
||
<cfloop array="#parsed.errors#" index="e">
|
||
<cfset arrayAppend(errors, zname & ": " & e)>
|
||
</cfloop>
|
||
<cfset zzis.closeEntry()>
|
||
</cfloop>
|
||
<cfset zzis.close()><cfset zbais.close()>
|
||
<cfcatch>
|
||
<cfset arrayAppend(errors, "ZIP: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
<cfelseif doc.mime_type EQ "application/pdf">
|
||
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
|
||
<cfset parsed = parsePDF(fileBytes)>
|
||
<cfset elements = parsed.elements>
|
||
<cfset errors = parsed.errors>
|
||
|
||
<cfelseif doc.mime_type EQ "application/vnd.openxmlformats-officedocument.wordprocessingml.document">
|
||
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
|
||
<cfset parsed = parseDOCX_POI(fileBytes)>
|
||
<cfif arrayLen(parsed.elements) EQ 0>
|
||
<!--- POI дал 0 элементов (в т.ч. SDT-only) — fallback на ZIP/XML --->
|
||
<cfset parsed = parseDOCX(fileBytes)>
|
||
</cfif>
|
||
<cfset elements = parsed.elements>
|
||
<cfset errors = parsed.errors>
|
||
|
||
<cfelseif doc.mime_type EQ "text/plain" OR doc.mime_type EQ "application/octet-stream">
|
||
<cftry>
|
||
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
|
||
<cfset text = createObject("java", "java.lang.String").init(fileBytes, "UTF-8")>
|
||
<cfloop list="#text#" index="line" delimiters="#chr(10)#">
|
||
<cfset line = trim(line)>
|
||
<cfif len(line)>
|
||
<cfset arrayAppend(elements, {type: "paragraph", style: "", text: line, page: 1})>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfcatch>
|
||
<cfset arrayAppend(errors, "TXT: " & cfcatch.message)>
|
||
</cfcatch>
|
||
</cftry>
|
||
|
||
<cfelseif doc.mime_type EQ "application/msword">
|
||
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
|
||
<!--- После конвертации на ВМ это уже docx — пробуем XWPF первым --->
|
||
<cftry>
|
||
<cfset parsed = parseDOCX(fileBytes)>
|
||
<cfcatch>
|
||
<cfset parsed = {elements: [], errors: ["XWPF fallback: " & cfcatch.message]}>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfif arrayLen(parsed.elements) EQ 0>
|
||
<cfset parsed = parseDOC_POI(fileBytes)>
|
||
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
|
||
<cfset parsed = parseDOC(fileBytes)>
|
||
</cfif>
|
||
</cfif>
|
||
<cfset elements = parsed.elements>
|
||
<cfset errors = parsed.errors>
|
||
<cfelse>
|
||
<cfset arrayAppend(errors, "unsupported: " & doc.mime_type)>
|
||
</cfif>
|
||
|
||
<cfset newStatus = arrayLen(elements) GT 0 ? "parsed" : "error">
|
||
<cfif arrayLen(errors) GT 0 AND arrayLen(elements) EQ 0>
|
||
<cfset newStatus = "error">
|
||
</cfif>
|
||
<cfquery datasource="baza">
|
||
UPDATE documents
|
||
SET elements_json = <cfqueryparam value="#serializeJSON(elements)#" cfsqltype="cf_sql_varchar">::jsonb,
|
||
status = <cfqueryparam value="#newStatus#" cfsqltype="cf_sql_varchar">,
|
||
error_message = <cfqueryparam value="#arrayLen(errors) ? serializeJSON(errors) : ''#" cfsqltype="cf_sql_varchar">
|
||
WHERE id = <cfqueryparam value="#url.doc_id#" cfsqltype="cf_sql_varchar">
|
||
</cfquery>
|
||
<!--- Статистика --->
|
||
<cfset paraCount = 0>
|
||
<cfset tblCount = 0>
|
||
<cfset tblRows = 0>
|
||
<cfset textPreview = "">
|
||
<cfset tblHeaders = []>
|
||
<cfloop array="#elements#" index="el">
|
||
<cfif el.type EQ "paragraph">
|
||
<cfset paraCount++>
|
||
<cfif len(textPreview) LT 200>
|
||
<cfset textPreview &= el.text & chr(10)>
|
||
</cfif>
|
||
<cfelseif el.type EQ "table">
|
||
<cfset tblCount++>
|
||
<cfset tblRows += arrayLen(el.rows)>
|
||
<cfif arrayLen(el.rows) GT 0>
|
||
<cfset arrayAppend(tblHeaders, el.rows[1])>
|
||
</cfif>
|
||
</cfif>
|
||
</cfloop>
|
||
<cfset textPreview = left(textPreview, 200)>
|
||
|
||
<cfset result = {ok: true, doc_id: url.doc_id, filename: doc.filename,
|
||
mime_type: doc.mime_type, status: newStatus,
|
||
element_count: arrayLen(elements), error_count: arrayLen(errors),
|
||
paragraphs: paraCount, tables: tblCount, table_rows: tblRows,
|
||
text_preview: textPreview, table_headers: tblHeaders,
|
||
errors: errors, files: files}>
|
||
</cfif>
|
||
</cfif>
|
||
<cfcatch>
|
||
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
|
||
</cfcatch>
|
||
</cftry>
|
||
<cfoutput>#serializeJSON(result)#</cfoutput>
|