From commits-return-15178-apmail-activemq-commits-archive=activemq.apache.org@activemq.apache.org Wed Dec 15 02:12:27 2010 Return-Path: Delivered-To: apmail-activemq-commits-archive@www.apache.org Received: (qmail 74928 invoked from network); 15 Dec 2010 02:12:27 -0000 Received: from unknown (HELO mail.apache.org) (140.211.11.3) by 140.211.11.9 with SMTP; 15 Dec 2010 02:12:27 -0000 Received: (qmail 7322 invoked by uid 500); 15 Dec 2010 02:12:27 -0000 Delivered-To: apmail-activemq-commits-archive@activemq.apache.org Received: (qmail 7295 invoked by uid 500); 15 Dec 2010 02:12:27 -0000 Mailing-List: contact commits-help@activemq.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@activemq.apache.org Delivered-To: mailing list commits@activemq.apache.org Received: (qmail 7288 invoked by uid 99); 15 Dec 2010 02:12:27 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 15 Dec 2010 02:12:27 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED,WEIRD_QUOTING X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 15 Dec 2010 02:12:23 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 1FF6A23889E7; Wed, 15 Dec 2010 02:12:03 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1049386 - in /activemq/activemq-apollo/trunk: apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ apollo-broker/src/test/scala/org/apache/activemq/apollo/web/ apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ apollo-dto/... Date: Wed, 15 Dec 2010 02:12:02 -0000 To: commits@activemq.apache.org From: chirino@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20101215021203.1FF6A23889E7@eris.apache.org> Author: chirino Date: Wed Dec 15 02:12:02 2010 New Revision: 1049386 URL: http://svn.apache.org/viewvc?rev=1049386&view=rev Log: Simplifying the BrokerDTO and FileConfigStore Added: activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/ activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala - copied, changed from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ValueDTO.java - copied, changed from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/web/resources/EditConfig.jade - copied, changed from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade Removed: activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala Modified: activemq/activemq-apollo/trunk/apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ConfigStore.scala activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/BrokerDTO.java activemq/activemq-apollo/trunk/apollo-dto/src/main/resources/org/apache/activemq/apollo/dto/jaxb.index activemq/activemq-apollo/trunk/apollo-web/src/main/scala/org/apache/activemq/apollo/web/resources/ConfigurationResource.scala activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerDTO.jade activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade Modified: activemq/activemq-apollo/trunk/apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ConfigStore.scala URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ConfigStore.scala?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ConfigStore.scala (original) +++ activemq/activemq-apollo/trunk/apollo-broker/src/main/scala/org/apache/activemq/apollo/broker/ConfigStore.scala Wed Dec 15 02:12:02 2010 @@ -16,24 +16,17 @@ */ package org.apache.activemq.apollo.broker -import org.apache.activemq.apollo.broker.jaxb.PropertiesReader -import org.apache.activemq.apollo.dto.{XmlCodec, ConnectorDTO, VirtualHostDTO, BrokerDTO} -import java.util.regex.Pattern -import javax.xml.stream.{XMLOutputFactory, XMLInputFactory} +import org.apache.activemq.apollo.dto.{XmlCodec, BrokerDTO} import org.fusesource.hawtdispatch._ -import _root_.org.fusesource.hawtdispatch.ScalaDispatchHelpers._ import java.util.concurrent.{TimeUnit, ExecutorService, Executors} import org.fusesource.hawtbuf.{ByteArrayInputStream, ByteArrayOutputStream} -import javax.xml.bind.{Marshaller, JAXBContext} import security.EncryptionSupport import XmlCodec._ import org.apache.activemq.apollo.util._ -import scala.util.continuations._ -import org.fusesource.hawtdispatch.DispatchQueue import java.util.Arrays import FileSupport._ import java.util.Properties -import java.io.{FileInputStream, OutputStreamWriter, File} +import java.io.{FileOutputStream, FileInputStream, File} object ConfigStore { @@ -54,9 +47,12 @@ object ConfigStore { */ trait ConfigStore { + def read(): String + def write(value:String): Unit + def load(eval:Boolean): BrokerDTO - def store(config:BrokerDTO): Boolean + def store(config:BrokerDTO): Unit def can_write:Boolean @@ -79,13 +75,7 @@ object FileConfigStore extends Log class FileConfigStore extends ConfigStore { import FileConfigStore._ - object StoredBrokerModel { - def apply(config:BrokerDTO) = { - val data = marshall(config) - new StoredBrokerModel(config.rev, data, 0) - } - } - case class StoredBrokerModel(rev:Int, data:Array[Byte], lastModified:Long) + case class StoredBrokerModel(data:Array[Byte], lastModified:Long) var file:File = new File("activemq.xml") @@ -106,29 +96,20 @@ class FileConfigStore extends ConfigStor running = true file = file.getCanonicalFile; - file.getParentFile.mkdir - // Find the latest rev - val files = file.getParentFile.listFiles - val regex = (Pattern.quote(file.getName+".")+"""(\d)$""").r - var revs:List[Int] = Nil - if( files!=null ) { - for( file <- files) { - file.getName match { - case regex(ver)=> - revs ::= Integer.parseInt(ver) - case _ => - } - } - } - revs = revs.sortWith((x,y)=> x < y) - val last = if( file.exists ) { - read(1, file) - } else { - write(StoredBrokerModel(defaultConfig(1))) + if( !file.exists ) { + try { + // try to create a default version of the file. + store(Broker.defaultConfig) + } catch { + case e:Throwable => + } + if( !file.exists ) { + throw new Exception("The '%s' configuration file does not exist.".format(file.getPath)) + } } - latest = last + latest = read(file) schedualNextUpdateCheck } @@ -141,38 +122,36 @@ class FileConfigStore extends ConfigStor unmarshall(latest.data, eval) } + def read() = { + new String(latest.data) + } + def can_write:Boolean = file.canWrite - def store(config:BrokerDTO) = { - debug("storing broker model: %s ver %d", config.id, config.rev) - if( latest.rev+1 != config.rev ) { - debug("update request does not match next revision: %d", latest.rev+1) - false - } else { - latest = write(StoredBrokerModel(config)) - true - } + def store(config:BrokerDTO):Unit = { + val data = marshall(config) + latest = write(StoredBrokerModel(data, 0)) } - private def fileRev(rev:Int) = new File(file.getParent, file.getName+"."+rev) + def write(value:String) = { + val m = StoredBrokerModel(value.getBytes, 0) + unmarshall(m.data) + latest = write(m) + } private def schedualNextUpdateCheck:Unit = dispatchQueue.after(1, TimeUnit.SECONDS) { if( running ) { val lastModified = latest.lastModified val latestData = latest.data - val nextRev = latest.rev+1 ioWorker { try { val l = file.lastModified if( l != lastModified ) { - val config = read(nextRev, file) + val config = read(file) if ( !Arrays.equals(latestData, config.data) ) { - val c = unmarshall(config.data) - c.rev = config.rev - store(c) - } else { - latest = latest.copy(lastModified = l) + // TODO: trigger reloading the config file. } + latest = config } schedualNextUpdateCheck } @@ -185,24 +164,24 @@ class FileConfigStore extends ConfigStor } } - - - private def defaultConfig(rev:Int) = { - val config = Broker.defaultConfig - config.rev = rev - config - } - - private def read(rev:Int, file: File) ={ + private def read(file: File) ={ val data = IOHelper.readBytes(file) val config = unmarshall(data) // validates the xml - StoredBrokerModel(rev, data, file.lastModified) + StoredBrokerModel(data, file.lastModified) } private def write(config:StoredBrokerModel) = { - // write to the files.. + + // backup the config file... + if(file.exists()) { + using(new FileInputStream(file)) { in => + using(new FileOutputStream(file.getParentFile / ("~"+file.getName))) { out => + copy(in, out) + } + } + } + IOHelper.writeBinaryFile(file, config.data) - IOHelper.writeBinaryFile(fileRev(config.rev), config.data) config.copy(lastModified = file.lastModified) } Copied: activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala (from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala) URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala?p2=activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala&p1=activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala&r1=1049385&r2=1049386&rev=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala (original) +++ activemq/activemq-apollo/trunk/apollo-broker/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala Wed Dec 15 02:12:02 2010 @@ -1,3 +1,5 @@ +package org.apache.activemq.apollo.web + /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -14,8 +16,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.apollo.web - import java.io.File import org.apache.activemq.apollo.util._ import org.apache.activemq.apollo.broker.FileConfigStore @@ -41,5 +41,4 @@ class FileConfigStoreTest extends FunSui store.stop } -} - +} \ No newline at end of file Modified: activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/BrokerDTO.java URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/BrokerDTO.java?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/BrokerDTO.java (original) +++ activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/BrokerDTO.java Wed Dec 15 02:12:02 2010 @@ -21,6 +21,8 @@ import java.util.ArrayList; import java.util.List; /** + * This is the root container for a broker's configuration. + * * @author Hiram Chirino */ @XmlRootElement(name="broker") @@ -28,18 +30,6 @@ import java.util.List; public class BrokerDTO extends ServiceDTO { /** - * Used to track config revisions. - */ - @XmlAttribute - public int rev; - - /** - * Used to track who last modified the configuration. - */ - @XmlAttribute(name="modified-by") - public String modified_by; - - /** * Used to store any configuration notes. */ @XmlElement Copied: activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ValueDTO.java (from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala) URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ValueDTO.java?p2=activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ValueDTO.java&p1=activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala&r1=1049385&r2=1049386&rev=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/test/scala/org/apache/activemq/apollo/web/FileConfigStoreTest.scala (original) +++ activemq/activemq-apollo/trunk/apollo-dto/src/main/java/org/apache/activemq/apollo/dto/ValueDTO.java Wed Dec 15 02:12:02 2010 @@ -14,32 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.apollo.web +package org.apache.activemq.apollo.dto; -import java.io.File -import org.apache.activemq.apollo.util._ -import org.apache.activemq.apollo.broker.FileConfigStore -import org.fusesource.hawtdispatch._ +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; /** - *

- *

+ * This is the root container for a broker's configuration. * * @author Hiram Chirino */ -class FileConfigStoreTest extends FunSuiteSupport { - test("file config store") { - - val store = new FileConfigStore - store.file = new File("activemq.xml") - - store.start - - expect("default") { - store.load(false).id - } - - store.stop - } +@XmlRootElement(name="value") +@XmlAccessorType(XmlAccessType.FIELD) +public class ValueDTO { + + /** + * Holds a value + */ + @XmlValue + public String value; } - Modified: activemq/activemq-apollo/trunk/apollo-dto/src/main/resources/org/apache/activemq/apollo/dto/jaxb.index URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-dto/src/main/resources/org/apache/activemq/apollo/dto/jaxb.index?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-dto/src/main/resources/org/apache/activemq/apollo/dto/jaxb.index (original) +++ activemq/activemq-apollo/trunk/apollo-dto/src/main/resources/org/apache/activemq/apollo/dto/jaxb.index Wed Dec 15 02:12:02 2010 @@ -45,4 +45,5 @@ QueueDTO DestinationDTO LinkDTO QueueConsumerStatusDTO -StompDTO \ No newline at end of file +StompDTO +ValueDTO \ No newline at end of file Modified: activemq/activemq-apollo/trunk/apollo-web/src/main/scala/org/apache/activemq/apollo/web/resources/ConfigurationResource.scala URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-web/src/main/scala/org/apache/activemq/apollo/web/resources/ConfigurationResource.scala?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/main/scala/org/apache/activemq/apollo/web/resources/ConfigurationResource.scala (original) +++ activemq/activemq-apollo/trunk/apollo-web/src/main/scala/org/apache/activemq/apollo/web/resources/ConfigurationResource.scala Wed Dec 15 02:12:02 2010 @@ -23,8 +23,10 @@ import Response.Status._ import Response._ import java.net.URI import java.io.ByteArrayInputStream -import org.apache.activemq.apollo.dto.{XmlCodec, BrokerDTO} import org.apache.activemq.apollo.broker.ConfigStore +import org.apache.activemq.apollo.dto.{ValueDTO, XmlCodec, BrokerDTO} + +case class EditConfig(config:String) /** * A broker resource is used to represent the configuration of a broker. @@ -32,45 +34,47 @@ import org.apache.activemq.apollo.broker @Produces(Array("application/json", "application/xml","text/xml", "text/html;qs=5")) case class ConfigurationResource(parent:BrokerResource) extends Resource(parent) { - lazy val config = { - val store = ConfigStore() - if( store.can_write ) { - store.load(false) + lazy val store = { + val rc = ConfigStore() + if( rc.can_write ) { + rc } else { None }.getOrElse(result(NOT_FOUND)) } + @GET + def get() = store.load(false) + @Produces(Array("text/html")) @GET - def get(@Context uriInfo:UriInfo) = { - val ub = uriInfo.getAbsolutePathBuilder() - seeOther(path(config.rev)).build + @Path("edit") + def edit_html() = { + EditConfig(store.read) } - @GET @Path("{rev}") - def getConfig(@PathParam("rev") rev:Int):BrokerDTO = { - // that rev may have gone away.. - config.rev==rev || result(NOT_FOUND) - config + @POST + @Path("edit") + def edit_post(@FormParam("config") config:String) = { + val rc = new ValueDTO + rc.value = config + edit_put(rc) + result(path("../..")) } - @POST @Path("{rev}") - def post(@PathParam("rev") rev:Int, @FormParam("config") config:String) = { - val dto = XmlCodec.unmarshalBrokerDTO(new ByteArrayInputStream(config.getBytes("UTF-8"))) - put(rev, dto) - result(path("../"+dto.rev)) + @Produces(Array("application/json", "application/xml","text/xml")) + @GET + @Path("edit") + def edit() = { + val rc = new ValueDTO + rc.value = store.read + rc } - @PUT @Path("{rev}") - def put(@PathParam("rev") rev:Int, config:BrokerDTO) = { - config.rev = rev - val store = ConfigStore() - if( store.can_write ) { - store.store(config) - } else { - false - } || result(NOT_FOUND) + @PUT + @Path("edit") + def edit_put(config:ValueDTO) = { + store.write(config.value) } } Modified: activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerDTO.jade URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerDTO.jade?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerDTO.jade (original) +++ activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerDTO.jade Wed Dec 15 02:12:02 2010 @@ -13,21 +13,11 @@ -# See the License for the specific language governing permissions and -# limitations under the License. +- import it._ - val helper = new org.apache.activemq.apollo.web.resources.ViewHelper - import helper._ -- import org.fusesource.hawtbuf._ -- import org.apache.activemq.apollo.dto.XmlCodec._ .breadcumbs a(href={strip_resolve("..")}) Back -form(method="post" action={it.rev+1}) - div - input(type="submit" value="Update") - div - textarea(rows="40" cols="80" name="config")< - - val baos = new ByteArrayOutputStream - - marshalBrokerDTO(it, baos, true) - ~~ new String(baos.toByteArray, "UTF-8") - div - input(type="submit" value="Update") \ No newline at end of file +a(href="config/edit") Edit Modified: activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade?rev=1049386&r1=1049385&r2=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade (original) +++ activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade Wed Dec 15 02:12:02 2010 @@ -25,5 +25,5 @@ h1 Broker: #{id} - if (configurable) p - a(href={path("config")}) configuration + a(href={path("config/edit")}) configuration Copied: activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/web/resources/EditConfig.jade (from r1049385, activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade) URL: http://svn.apache.org/viewvc/activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/web/resources/EditConfig.jade?p2=activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/web/resources/EditConfig.jade&p1=activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade&r1=1049385&r2=1049386&rev=1049386&view=diff ============================================================================== --- activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/dto/BrokerSummaryDTO.jade (original) +++ activemq/activemq-apollo/trunk/apollo-web/src/main/webapp/WEB-INF/org/apache/activemq/apollo/web/resources/EditConfig.jade Wed Dec 15 02:12:02 2010 @@ -4,9 +4,9 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at --# +-# -# http://www.apache.org/licenses/LICENSE-2.0 --# +-# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,13 +17,14 @@ - val helper = new org.apache.activemq.apollo.web.resources.ViewHelper - import helper._ -h1 Broker: #{id} - -- if (manageable) - p - a(href={path("runtime")}) manage - -- if (configurable) - p - a(href={path("config")}) configuration +.breadcumbs + a(href={strip_resolve("../..")}) Back +form(method="post" action="edit") + div + input(type="submit" value="Update") + div + textarea(rows="40" cols="80" name="config")< + ~~ config + div + input(type="submit" value="Update") \ No newline at end of file